|
1
|
|
|
/** |
|
2
|
|
|
* EGroupware clientside Application javascript base object |
|
3
|
|
|
* |
|
4
|
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License |
|
5
|
|
|
* @package etemplate |
|
6
|
|
|
* @subpackage api |
|
7
|
|
|
* @link http://www.egroupware.org |
|
8
|
|
|
* @author Ralf Becker <[email protected]> |
|
9
|
|
|
* @author Hadi Nategh <[email protected]> |
|
10
|
|
|
* @author Nathan Gray <[email protected]> |
|
11
|
|
|
*/ |
|
12
|
|
|
|
|
13
|
|
|
import 'jquery'; |
|
14
|
|
|
import 'jqueryui'; |
|
15
|
|
|
import '../jsapi/egw_global'; |
|
16
|
|
|
import '../etemplate/et2_types'; |
|
17
|
|
|
|
|
18
|
|
|
/** |
|
19
|
|
|
* Type for push-message |
|
20
|
|
|
*/ |
|
21
|
|
|
export interface PushData |
|
22
|
|
|
{ |
|
23
|
|
|
type: "add"|"edit"|"update"|"delete"|"unknown"; |
|
24
|
|
|
app: string; // app-name, can include a subtype eg. "projectmanager-element" |
|
25
|
|
|
id: string | number; |
|
26
|
|
|
acl?: any; // app-specific acl data, eg. the owner, or array of participants |
|
27
|
|
|
account_id: number; // user that caused the change |
|
28
|
|
|
[propName:string]: any; // arbitrary more parameters |
|
29
|
|
|
} |
|
30
|
|
|
|
|
31
|
|
|
/** |
|
32
|
|
|
* Common base class for application javascript |
|
33
|
|
|
* Each app should extend as needed. |
|
34
|
|
|
* |
|
35
|
|
|
* All application javascript should be inside. Intitialization goes in init(), |
|
36
|
|
|
* clean-up code goes in destroy(). Initialization is done once all js is loaded. |
|
37
|
|
|
* |
|
38
|
|
|
* var app.appname = AppJS.extend({ |
|
39
|
|
|
* // Actually set this one, the rest is example |
|
40
|
|
|
* appname: appname, |
|
41
|
|
|
* |
|
42
|
|
|
* internal_var: 1000, |
|
43
|
|
|
* |
|
44
|
|
|
* init: function() |
|
45
|
|
|
* { |
|
46
|
|
|
* // Call the super |
|
47
|
|
|
* this._super.apply(this, arguments); |
|
48
|
|
|
* |
|
49
|
|
|
* // Init the stuff |
|
50
|
|
|
* if ( egw.preference('dateformat', 'common') ) |
|
51
|
|
|
* { |
|
52
|
|
|
* // etc |
|
53
|
|
|
* } |
|
54
|
|
|
* }, |
|
55
|
|
|
* _private: function() |
|
56
|
|
|
* { |
|
57
|
|
|
* // Underscore private by convention |
|
58
|
|
|
* } |
|
59
|
|
|
* }); |
|
60
|
|
|
*/ |
|
61
|
|
|
export abstract class EgwApp |
|
62
|
|
|
{ |
|
63
|
|
|
/** |
|
64
|
|
|
* Internal application name - override this |
|
65
|
|
|
*/ |
|
66
|
|
|
readonly appname: string; |
|
67
|
|
|
|
|
68
|
|
|
/** |
|
69
|
|
|
* Internal reference to the most recently loaded etemplate2 widget tree |
|
70
|
|
|
* |
|
71
|
|
|
* NOTE: This variable can change which etemplate it points to as the user |
|
72
|
|
|
* works. For example, loading the home or admin apps can cause |
|
73
|
|
|
* et2_ready() to be called again with a different template. this.et2 will |
|
74
|
|
|
* then point to a different template. If the user then closes that tab, |
|
75
|
|
|
* this.et2 will point to a destroyed object, and trying to use it will fail. |
|
76
|
|
|
* |
|
77
|
|
|
* If you need a reference to a certain template you can either store a local |
|
78
|
|
|
* reference or access it through etemplate2. |
|
79
|
|
|
* |
|
80
|
|
|
* @example <caption>Store a local reference</caption> |
|
81
|
|
|
* // in et2_ready() |
|
82
|
|
|
* if(name == 'index') this.index_et2 = et2.widgetContainer; |
|
83
|
|
|
* |
|
84
|
|
|
* // Remember to clean up in destroy() |
|
85
|
|
|
* delete this.index_et2; |
|
86
|
|
|
* |
|
87
|
|
|
* // Instead of this.et2, using a local reference |
|
88
|
|
|
* this.index_et2 ... |
|
89
|
|
|
* |
|
90
|
|
|
* |
|
91
|
|
|
* @example <caption>Access via etemplate2 object</caption> |
|
92
|
|
|
* // Instead of this.et2, using it's unique ID |
|
93
|
|
|
* var et2 = etemplate2.getById('myapp-index) |
|
94
|
|
|
* if(et2) |
|
95
|
|
|
* { |
|
96
|
|
|
* et2.widgetContainer. ... |
|
97
|
|
|
* } |
|
98
|
|
|
* |
|
99
|
|
|
* @var {et2_container} |
|
100
|
|
|
*/ |
|
101
|
|
|
et2: any; |
|
102
|
|
|
|
|
103
|
|
|
/** |
|
104
|
|
|
* Internal reference to egw client-side api object for current app and window |
|
105
|
|
|
* |
|
106
|
|
|
* @var {egw} |
|
107
|
|
|
*/ |
|
108
|
|
|
egw: IegwAppLocal; |
|
109
|
|
|
|
|
110
|
|
|
sidebox: JQuery; |
|
111
|
|
|
|
|
112
|
|
|
viewContainer: JQuery; |
|
113
|
|
|
viewTemplate: JQuery; |
|
114
|
|
|
et2_view: any; |
|
115
|
|
|
favorite_popup : JQuery | any; |
|
116
|
|
|
|
|
117
|
|
|
tutorial_initialised: boolean; |
|
118
|
|
|
|
|
119
|
|
|
dom_id : string; |
|
120
|
|
|
|
|
121
|
|
|
mailvelopeSyncHandlerObj : any; |
|
122
|
|
|
|
|
123
|
|
|
/** |
|
124
|
|
|
* Initialization and setup goes here, but the etemplate2 object |
|
125
|
|
|
* is not yet ready. |
|
126
|
|
|
*/ |
|
127
|
|
|
constructor() |
|
128
|
|
|
{ |
|
129
|
|
|
this.egw = egw(this.appname, window); |
|
130
|
|
|
|
|
131
|
|
|
// Initialize sidebox for non-popups. |
|
132
|
|
|
// ID set server side |
|
133
|
|
|
if(!this.egw.is_popup()) |
|
134
|
|
|
{ |
|
135
|
|
|
var sidebox = jQuery('#favorite_sidebox_'+this.appname); |
|
136
|
|
|
if(sidebox.length == 0 && egw_getFramework() != null) |
|
137
|
|
|
{ |
|
138
|
|
|
var egw_fw = egw_getFramework(); |
|
139
|
|
|
sidebox= jQuery('#favorite_sidebox_'+this.appname,egw_fw.sidemenuDiv); |
|
140
|
|
|
} |
|
141
|
|
|
// Make sure we're running in the top window when we init sidebox |
|
142
|
|
|
//@ts-ignore |
|
143
|
|
|
if(window.app[this.appname] === this && window.top.app[this.appname] !== this && window.top.app[this.appname]) |
|
144
|
|
|
{ |
|
145
|
|
|
//@ts-ignore |
|
146
|
|
|
window.top.app[this.appname]._init_sidebox(sidebox); |
|
147
|
|
|
} |
|
148
|
|
|
else |
|
149
|
|
|
{ |
|
150
|
|
|
this._init_sidebox(sidebox); |
|
151
|
|
|
} |
|
152
|
|
|
} |
|
153
|
|
|
this.mailvelopeSyncHandlerObj = this.mailvelopeSyncHandler(); |
|
154
|
|
|
} |
|
155
|
|
|
|
|
156
|
|
|
/** |
|
157
|
|
|
* Clean up any created objects & references |
|
158
|
|
|
* @param {object} _app local app object |
|
159
|
|
|
*/ |
|
160
|
|
|
destroy(_app) |
|
161
|
|
|
{ |
|
162
|
|
|
delete this.et2; |
|
163
|
|
|
if (this.sidebox) |
|
164
|
|
|
this.sidebox.off(); |
|
165
|
|
|
delete this.sidebox; |
|
166
|
|
|
if (!_app) delete app[this.appname]; |
|
167
|
|
|
} |
|
168
|
|
|
|
|
169
|
|
|
/** |
|
170
|
|
|
* This function is called when the etemplate2 object is loaded |
|
171
|
|
|
* and ready. If you must store a reference to the et2 object, |
|
172
|
|
|
* make sure to clean it up in destroy(). Note that this can be called |
|
173
|
|
|
* several times, with different et2 objects, as templates are loaded. |
|
174
|
|
|
* |
|
175
|
|
|
* @param {etemplate2} et2 |
|
176
|
|
|
* @param {string} name template name |
|
177
|
|
|
*/ |
|
178
|
|
|
et2_ready(et2, name : string) |
|
179
|
|
|
{ |
|
180
|
|
|
if(this.et2 !== null) |
|
181
|
|
|
{ |
|
182
|
|
|
egw.debug('log', "Changed et2 object"); |
|
183
|
|
|
} |
|
184
|
|
|
this.et2 = et2.widgetContainer; |
|
185
|
|
|
this._fix_iFrameScrolling(); |
|
186
|
|
|
if (this.egw && this.egw.is_popup()) this._set_Window_title(); |
|
187
|
|
|
|
|
188
|
|
|
// Highlights the favorite based on initial list state |
|
189
|
|
|
this.highlight_favorite(); |
|
190
|
|
|
} |
|
191
|
|
|
|
|
192
|
|
|
/** |
|
193
|
|
|
* Observer method receives update notifications from all applications |
|
194
|
|
|
* |
|
195
|
|
|
* App is responsible for only reacting to "messages" it is interested in! |
|
196
|
|
|
* |
|
197
|
|
|
* @param {string} _msg message (already translated) to show, eg. 'Entry deleted' |
|
198
|
|
|
* @param {string} _app application name |
|
199
|
|
|
* @param {(string|number)} _id id of entry to refresh or null |
|
200
|
|
|
* @param {string} _type either 'update', 'edit', 'delete', 'add' or null |
|
201
|
|
|
* - update: request just modified data from given rows. Sorting is not considered, |
|
202
|
|
|
* so if the sort field is changed, the row will not be moved. |
|
203
|
|
|
* - edit: rows changed, but sorting may be affected. Requires full reload. |
|
204
|
|
|
* - delete: just delete the given rows clientside (no server interaction neccessary) |
|
205
|
|
|
* - add: requires full reload for proper sorting |
|
206
|
|
|
* @param {string} _msg_type 'error', 'warning' or 'success' (default) |
|
207
|
|
|
* @param {object|null} _links app => array of ids of linked entries |
|
208
|
|
|
* or null, if not triggered on server-side, which adds that info |
|
209
|
|
|
* @return {false|*} false to stop regular refresh, thought all observers are run |
|
210
|
|
|
*/ |
|
211
|
|
|
observer(_msg, _app, _id, _type, _msg_type, _links) |
|
212
|
|
|
{ |
|
213
|
|
|
|
|
214
|
|
|
} |
|
215
|
|
|
|
|
216
|
|
|
/** |
|
217
|
|
|
* Handle a push notification about entry changes from the websocket |
|
218
|
|
|
* |
|
219
|
|
|
* Get's called for data of all apps, but should only handle data of apps it displays, |
|
220
|
|
|
* which is by default only it's own, but can be for multiple apps eg. for calendar. |
|
221
|
|
|
* |
|
222
|
|
|
* @param pushData |
|
223
|
|
|
* @param {string} pushData.app application name |
|
224
|
|
|
* @param {(string|number)} pushData.id id of entry to refresh or null |
|
225
|
|
|
* @param {string} pushData.type either 'update', 'edit', 'delete', 'add' or null |
|
226
|
|
|
* - update: request just modified data from given rows. Sorting is not considered, |
|
227
|
|
|
* so if the sort field is changed, the row will not be moved. |
|
228
|
|
|
* - edit: rows changed, but sorting may be affected. Requires full reload. |
|
229
|
|
|
* - delete: just delete the given rows clientside (no server interaction neccessary) |
|
230
|
|
|
* - add: requires full reload for proper sorting |
|
231
|
|
|
* @param {object|null} pushData.acl Extra data for determining relevance. eg: owner or responsible to decide if update is necessary |
|
232
|
|
|
* @param {number} pushData.account_id User that caused the notification |
|
233
|
|
|
*/ |
|
234
|
|
|
push(pushData : PushData) |
|
235
|
|
|
{ |
|
236
|
|
|
// don't care about other apps data, reimplement if your app does care eg. calendar |
|
237
|
|
|
if (pushData.app !== this.appname) return; |
|
238
|
|
|
|
|
239
|
|
|
// only handle delete by default, for simple case of uid === "$app::$id" |
|
240
|
|
|
if (pushData.type === 'delete') |
|
241
|
|
|
{ |
|
242
|
|
|
egw.dataStoreUID(this.uid(pushData), null); |
|
243
|
|
|
} |
|
244
|
|
|
} |
|
245
|
|
|
|
|
246
|
|
|
/** |
|
247
|
|
|
* Get (possible) app-specific uid |
|
248
|
|
|
* |
|
249
|
|
|
* @param {object} pushData see push method for individual attributes |
|
250
|
|
|
*/ |
|
251
|
|
|
uid(pushData) |
|
252
|
|
|
{ |
|
253
|
|
|
return pushData.app + '::' + pushData.id; |
|
254
|
|
|
} |
|
255
|
|
|
|
|
256
|
|
|
/** |
|
257
|
|
|
* Method called after apps push implementation checked visibility |
|
258
|
|
|
* |
|
259
|
|
|
* @param {et2_nextmatch} nm |
|
260
|
|
|
* @param pushData see push method for individual attributes |
|
261
|
|
|
* @todo implement better way to update nextmatch widget without disturbing the user / state |
|
262
|
|
|
* @todo show indicator that an update has happend |
|
263
|
|
|
* @todo rate-limit update frequency |
|
264
|
|
|
*/ |
|
265
|
|
|
updateList(nm, pushData : PushData) |
|
266
|
|
|
{ |
|
267
|
|
|
switch (pushData.type) |
|
268
|
|
|
{ |
|
269
|
|
|
case 'add': |
|
270
|
|
|
case 'unknown': |
|
271
|
|
|
nm.applyFilters(); |
|
272
|
|
|
break; |
|
273
|
|
|
|
|
274
|
|
|
default: |
|
275
|
|
|
egw.dataRefreshUID(this.uid(pushData)); |
|
276
|
|
|
break; |
|
277
|
|
|
} |
|
278
|
|
|
} |
|
279
|
|
|
|
|
280
|
|
|
/** |
|
281
|
|
|
* Open an entry. |
|
282
|
|
|
* |
|
283
|
|
|
* Designed to be used with the action system as a callback |
|
284
|
|
|
* eg: onExecute => app.<appname>.open |
|
285
|
|
|
* |
|
286
|
|
|
* @param _action |
|
287
|
|
|
* @param _senders |
|
288
|
|
|
*/ |
|
289
|
|
|
open(_action, _senders) { |
|
290
|
|
|
var id_app = _senders[0].id.split('::'); |
|
291
|
|
|
egw.open(id_app[1], this.appname); |
|
292
|
|
|
} |
|
293
|
|
|
|
|
294
|
|
|
_do_action(action_id : string, selected : []) |
|
295
|
|
|
{ |
|
296
|
|
|
} |
|
297
|
|
|
|
|
298
|
|
|
/** |
|
299
|
|
|
* A generic method to action to server asynchronously |
|
300
|
|
|
* |
|
301
|
|
|
* Designed to be used with the action system as a callback. |
|
302
|
|
|
* In the PHP side, set the action |
|
303
|
|
|
* 'onExecute' => 'javaScript:app.<appname>.action', and |
|
304
|
|
|
* implement _do_action(action_id, selected) |
|
305
|
|
|
* |
|
306
|
|
|
* @param {egwAction} _action |
|
307
|
|
|
* @param {egwActionObject[]} _elems |
|
308
|
|
|
*/ |
|
309
|
|
|
action(_action, _elems) |
|
310
|
|
|
{ |
|
311
|
|
|
// let user confirm select-all |
|
312
|
|
|
var select_all = _action.getManager().getActionById("select_all"); |
|
313
|
|
|
var confirm_msg = (_elems.length > 1 || select_all && select_all.checked) && |
|
314
|
|
|
typeof _action.data.confirm_multiple != 'undefined' ? |
|
315
|
|
|
_action.data.confirm_multiple : _action.data.confirm; |
|
316
|
|
|
|
|
317
|
|
|
if (typeof confirm_msg != 'undefined') |
|
318
|
|
|
{ |
|
319
|
|
|
var that = this; |
|
320
|
|
|
var action_id = _action.id; |
|
321
|
|
|
et2_dialog.show_dialog(function(button_id,value) |
|
322
|
|
|
{ |
|
323
|
|
|
if (button_id != et2_dialog.NO_BUTTON) |
|
324
|
|
|
{ |
|
325
|
|
|
that._do_action(action_id, _elems); |
|
326
|
|
|
} |
|
327
|
|
|
}, confirm_msg, egw.lang('Confirmation required'), et2_dialog.BUTTONS_YES_NO, et2_dialog.QUESTION_MESSAGE); |
|
328
|
|
|
} |
|
329
|
|
|
else if (typeof this._do_action == 'function') |
|
330
|
|
|
{ |
|
331
|
|
|
this._do_action(_action.id, _elems); |
|
332
|
|
|
} |
|
333
|
|
|
else |
|
334
|
|
|
{ |
|
335
|
|
|
// If this is a nextmatch action, do an ajax submit setting the action |
|
336
|
|
|
var nm = null; |
|
337
|
|
|
var action = _action; |
|
338
|
|
|
while(nm == null && action.parent != null) |
|
339
|
|
|
{ |
|
340
|
|
|
if(action.data.nextmatch) nm = action.data.nextmatch; |
|
341
|
|
|
action = action.parent; |
|
342
|
|
|
} |
|
343
|
|
|
if(nm != null) |
|
344
|
|
|
{ |
|
345
|
|
|
var value = {}; |
|
346
|
|
|
value[nm.options.settings.action_var] = _action.id; |
|
347
|
|
|
nm.set_value(value); |
|
348
|
|
|
nm.getInstanceManager().submit(); |
|
349
|
|
|
} |
|
350
|
|
|
} |
|
351
|
|
|
} |
|
352
|
|
|
|
|
353
|
|
|
/** |
|
354
|
|
|
* Set the application's state to the given state. |
|
355
|
|
|
* |
|
356
|
|
|
* While not pretending to implement the history API, it is patterned similarly |
|
357
|
|
|
* @link http://www.whatwg.org/specs/web-apps/current-work/multipage/history.html |
|
358
|
|
|
* |
|
359
|
|
|
* The default implementation works with the favorites to apply filters to a nextmatch. |
|
360
|
|
|
* |
|
361
|
|
|
* |
|
362
|
|
|
* @param {{name: string, state: object}|string} state Object (or JSON string) for a state. |
|
363
|
|
|
* Only state is required, and its contents are application specific. |
|
364
|
|
|
* @param {string} template template name to check, instead of trying all templates of current app |
|
365
|
|
|
* @return {boolean} false - Returns false to stop event propagation |
|
366
|
|
|
*/ |
|
367
|
|
|
setState(state, template? : string) : string|false|void |
|
368
|
|
|
{ |
|
369
|
|
|
// State should be an object, not a string, but we'll parse |
|
370
|
|
|
if(typeof state == "string") |
|
371
|
|
|
{ |
|
372
|
|
|
if(state.indexOf('{') != -1 || state =='null') |
|
373
|
|
|
{ |
|
374
|
|
|
state = JSON.parse(state); |
|
375
|
|
|
} |
|
376
|
|
|
} |
|
377
|
|
|
if(typeof state != "object") |
|
378
|
|
|
{ |
|
379
|
|
|
egw.debug('error', 'Unable to set state to %o, needs to be an object',state); |
|
380
|
|
|
return; |
|
381
|
|
|
} |
|
382
|
|
|
if(state == null) |
|
383
|
|
|
{ |
|
384
|
|
|
state = {}; |
|
385
|
|
|
} |
|
386
|
|
|
|
|
387
|
|
|
// Check for egw.open() parameters |
|
388
|
|
|
if(state.state && state.state.id && state.state.app) |
|
389
|
|
|
{ |
|
390
|
|
|
return egw.open(state.state,undefined,undefined,{},'_self'); |
|
391
|
|
|
} |
|
392
|
|
|
|
|
393
|
|
|
// Try and find a nextmatch widget, and set its filters |
|
394
|
|
|
var nextmatched = false; |
|
395
|
|
|
var et2 = template ? etemplate2.getByTemplate(template) : etemplate2.getByApplication(this.appname); |
|
396
|
|
|
for(var i = 0; i < et2.length; i++) |
|
397
|
|
|
{ |
|
398
|
|
|
et2[i].widgetContainer.iterateOver(function(_widget) { |
|
399
|
|
|
// Firefox has trouble with spaces in search |
|
400
|
|
|
if(state.state && state.state.search) state.state.search = unescape(state.state.search); |
|
401
|
|
|
|
|
402
|
|
|
// Apply |
|
403
|
|
|
if(state.state && state.state.sort && state.state.sort.id) |
|
404
|
|
|
{ |
|
405
|
|
|
_widget.sortBy(state.state.sort.id, state.state.sort.asc,false); |
|
406
|
|
|
} |
|
407
|
|
|
if(state.state && state.state.selectcols) |
|
408
|
|
|
{ |
|
409
|
|
|
// Make sure it's a real array, not an object, then set cols |
|
410
|
|
|
_widget.set_columns(jQuery.extend([],state.state.selectcols)); |
|
411
|
|
|
} |
|
412
|
|
|
_widget.applyFilters(state.state || state.filter || {}); |
|
413
|
|
|
nextmatched = true; |
|
414
|
|
|
}, this, et2_nextmatch); |
|
415
|
|
|
if(nextmatched) return false; |
|
416
|
|
|
} |
|
417
|
|
|
|
|
418
|
|
|
// 'blank' is the special name for no filters, send that instead of the nice translated name |
|
419
|
|
|
var safe_name = jQuery.isEmptyObject(state) || jQuery.isEmptyObject(state.state||state.filter) ? 'blank' : state.name.replace(/[^A-Za-z0-9-_]/g, '_'); |
|
420
|
|
|
var url = '/'+this.appname+'/index.php'; |
|
421
|
|
|
|
|
422
|
|
|
// Try a redirect to list, if app defines a "list" value in registry |
|
423
|
|
|
if (egw.link_get_registry(this.appname, 'list')) |
|
424
|
|
|
{ |
|
425
|
|
|
url = egw.link('/index.php', jQuery.extend({'favorite': safe_name}, egw.link_get_registry(this.appname, 'list'))); |
|
426
|
|
|
} |
|
427
|
|
|
// if no list try index value from application |
|
428
|
|
|
else if (egw.app(this.appname)?.index) |
|
429
|
|
|
{ |
|
430
|
|
|
url = egw.link('/index.php', 'menuaction='+egw.app(this.appname).index+'&favorite='+safe_name); |
|
431
|
|
|
} |
|
432
|
|
|
egw.open_link(url, undefined, undefined, this.appname); |
|
433
|
|
|
return false; |
|
434
|
|
|
} |
|
435
|
|
|
|
|
436
|
|
|
/** |
|
437
|
|
|
* Retrieve the current state of the application for future restoration |
|
438
|
|
|
* |
|
439
|
|
|
* The state can be anything, as long as it's an object. The contents are |
|
440
|
|
|
* application specific. The default implementation finds a nextmatch and |
|
441
|
|
|
* returns its value. |
|
442
|
|
|
* The return value of this function cannot be passed directly to setState(), |
|
443
|
|
|
* since setState is expecting an additional wrapper, eg: |
|
444
|
|
|
* {name: 'something', state: getState()} |
|
445
|
|
|
* |
|
446
|
|
|
* @return {object} Application specific map representing the current state |
|
447
|
|
|
*/ |
|
448
|
|
|
getState() : {[propName:string]: any} |
|
449
|
|
|
{ |
|
450
|
|
|
var state = {}; |
|
451
|
|
|
|
|
452
|
|
|
// Try and find a nextmatch widget, and set its filters |
|
453
|
|
|
var et2 = etemplate2.getByApplication(this.appname); |
|
454
|
|
|
for(var i = 0; i < et2.length; i++) |
|
455
|
|
|
{ |
|
456
|
|
|
et2[i].widgetContainer.iterateOver(function(_widget) { |
|
457
|
|
|
state = _widget.getValue(); |
|
458
|
|
|
}, this, et2_nextmatch); |
|
459
|
|
|
} |
|
460
|
|
|
|
|
461
|
|
|
return state; |
|
462
|
|
|
} |
|
463
|
|
|
|
|
464
|
|
|
/** |
|
465
|
|
|
* Function to load selected row from nm into a template view |
|
466
|
|
|
* |
|
467
|
|
|
* @param {object} _action |
|
468
|
|
|
* @param {object} _senders |
|
469
|
|
|
* @param {boolean} _noEdit defines whether to set edit button or not default is false |
|
470
|
|
|
* @param {function} et2_callback function to run after et2 is loaded |
|
471
|
|
|
*/ |
|
472
|
|
|
viewEntry(_action, _senders, _noEdit, et2_callback) |
|
473
|
|
|
{ |
|
474
|
|
|
//full id in nm |
|
475
|
|
|
var id = _senders[0].id; |
|
476
|
|
|
// flag for edit button |
|
477
|
|
|
var noEdit = _noEdit || false; |
|
478
|
|
|
// nm row id |
|
479
|
|
|
var rowID = ''; |
|
480
|
|
|
// content to feed to etemplate2 |
|
481
|
|
|
var content:any = {}; |
|
482
|
|
|
|
|
483
|
|
|
var self = this; |
|
484
|
|
|
|
|
485
|
|
|
if (id){ |
|
486
|
|
|
var parts = id.split('::'); |
|
487
|
|
|
rowID = parts[1]; |
|
488
|
|
|
content = egw.dataGetUIDdata(id); |
|
489
|
|
|
if (content.data) content = content.data; |
|
490
|
|
|
} |
|
491
|
|
|
|
|
492
|
|
|
// create a new app object with just constructors for our new etemplate2 object |
|
493
|
|
|
var app = { classes: window.app.classes }; |
|
494
|
|
|
|
|
495
|
|
|
/* destroy generated etemplate for view mode in DOM*/ |
|
496
|
|
|
var destroy = function(){ |
|
497
|
|
|
self.viewContainer.remove(); |
|
498
|
|
|
delete self.viewTemplate; |
|
499
|
|
|
delete self.viewContainer; |
|
500
|
|
|
delete self.et2_view; |
|
501
|
|
|
// we need to reference back into parent context this |
|
502
|
|
|
for (var v in self) |
|
503
|
|
|
{ |
|
504
|
|
|
this[v] = self[v]; |
|
505
|
|
|
} |
|
506
|
|
|
app = null; |
|
507
|
|
|
}; |
|
508
|
|
|
|
|
509
|
|
|
// view container |
|
510
|
|
|
this.viewContainer = jQuery(document.createElement('div')) |
|
511
|
|
|
.addClass('et2_mobile_view') |
|
512
|
|
|
.css({ |
|
513
|
|
|
"z-index":102, |
|
514
|
|
|
width:"100%", |
|
515
|
|
|
height:"100%", |
|
516
|
|
|
background:"white", |
|
517
|
|
|
display:'block', |
|
518
|
|
|
position: 'absolute', |
|
519
|
|
|
left:0, |
|
520
|
|
|
bottom:0, |
|
521
|
|
|
right:0, |
|
522
|
|
|
overflow:'auto' |
|
523
|
|
|
}) |
|
524
|
|
|
.attr('id','popupMainDiv') |
|
525
|
|
|
.appendTo('body'); |
|
526
|
|
|
|
|
527
|
|
|
// close button |
|
528
|
|
|
var close = jQuery(document.createElement('span')) |
|
529
|
|
|
.addClass('egw_fw_mobile_popup_close loaded') |
|
530
|
|
|
.click(function(){ |
|
531
|
|
|
destroy.call(app[self.appname]); |
|
532
|
|
|
//disable selected actions after close |
|
533
|
|
|
egw_globalObjectManager.setAllSelected(false); |
|
534
|
|
|
}) |
|
535
|
|
|
.appendTo(this.viewContainer); |
|
536
|
|
|
if (!noEdit) |
|
537
|
|
|
{ |
|
538
|
|
|
// edit button |
|
539
|
|
|
var edit = jQuery(document.createElement('span')) |
|
540
|
|
|
.addClass('mobile-view-editBtn') |
|
541
|
|
|
.click(function(){ |
|
542
|
|
|
egw.open(rowID, self.appname); |
|
543
|
|
|
}) |
|
544
|
|
|
.appendTo(this.viewContainer); |
|
545
|
|
|
} |
|
546
|
|
|
// view template main container (content) |
|
547
|
|
|
this.viewTemplate = jQuery(document.createElement('div')) |
|
548
|
|
|
.attr('id', this.appname+'-view') |
|
549
|
|
|
.addClass('et2_mobile-view-container popupMainDiv') |
|
550
|
|
|
.appendTo(this.viewContainer); |
|
551
|
|
|
|
|
552
|
|
|
var mobileViewTemplate = (_action.data.mobileViewTemplate ||'edit').split('?'); |
|
553
|
|
|
var templateName = mobileViewTemplate[0]; |
|
554
|
|
|
var templateTimestamp = mobileViewTemplate[1]; |
|
555
|
|
|
var templateURL = egw.webserverUrl+ '/' + this.appname + '/templates/mobile/'+templateName+'.xet'+'?'+templateTimestamp; |
|
556
|
|
|
|
|
557
|
|
|
var data = { |
|
558
|
|
|
'content': content, |
|
559
|
|
|
'readonlys': {'__ALL__':true,'link_to':false}, |
|
560
|
|
|
'currentapp': this.appname, |
|
561
|
|
|
'langRequire': this.et2.getArrayMgr('langRequire').data, |
|
562
|
|
|
'sel_options': this.et2.getArrayMgr('sel_options').data, |
|
563
|
|
|
'modifications': this.et2.getArrayMgr('modifications').data, |
|
564
|
|
|
'validation_errors': this.et2.getArrayMgr('validation_errors').data |
|
565
|
|
|
}; |
|
566
|
|
|
|
|
567
|
|
|
// etemplate2 object for view |
|
568
|
|
|
this.et2_view = new etemplate2 (this.viewTemplate[0], false); |
|
569
|
|
|
framework.pushState('view'); |
|
570
|
|
|
if(templateName) |
|
571
|
|
|
{ |
|
572
|
|
|
this.et2_view.load(this.appname+'.'+templateName,templateURL, data, typeof et2_callback == 'function'?et2_callback:function(){}, app); |
|
573
|
|
|
} |
|
574
|
|
|
|
|
575
|
|
|
// define a global close function for view template |
|
576
|
|
|
// in order to be able to destroy view on action |
|
577
|
|
|
this.et2_view.close = destroy; |
|
578
|
|
|
} |
|
579
|
|
|
|
|
580
|
|
|
/** |
|
581
|
|
|
* Initializes actions and handlers on sidebox (delete) |
|
582
|
|
|
* |
|
583
|
|
|
* @param {jQuery} sidebox jQuery of DOM node |
|
584
|
|
|
*/ |
|
585
|
|
|
_init_sidebox(sidebox) |
|
586
|
|
|
{ |
|
587
|
|
|
// Initialize egw tutorial sidebox, but only for non-popups, as calendar edit app.js has this.et2 set to tutorial et2 object |
|
588
|
|
|
if (!this.egw.is_popup()) |
|
589
|
|
|
{ |
|
590
|
|
|
var egw_fw = egw_getFramework(); |
|
591
|
|
|
var tutorial = jQuery('#egw_tutorial_'+this.appname+'_sidebox', egw_fw ? egw_fw.sidemenuDiv : document); |
|
592
|
|
|
// _init_sidebox gets currently called multiple times, which needs to be fixed |
|
593
|
|
|
if (tutorial.length && !this.tutorial_initialised) |
|
594
|
|
|
{ |
|
595
|
|
|
this.egwTutorial_init(tutorial[0]); |
|
596
|
|
|
this.tutorial_initialised = true; |
|
597
|
|
|
} |
|
598
|
|
|
} |
|
599
|
|
|
if(sidebox.length) |
|
600
|
|
|
{ |
|
601
|
|
|
var self = this; |
|
602
|
|
|
if(this.sidebox) this.sidebox.off(); |
|
603
|
|
|
this.sidebox = sidebox; |
|
604
|
|
|
sidebox |
|
605
|
|
|
.off() |
|
606
|
|
|
// removed .on("mouse(enter|leave)" (wrapping trash icon), as it stalls delete in IE11 |
|
607
|
|
|
.on("click.sidebox","div.ui-icon-trash", this, this.delete_favorite) |
|
608
|
|
|
// need to install a favorite handler, as we switch original one off with .off() |
|
609
|
|
|
.on('click.sidebox','li[data-id]', this, function(event) { |
|
610
|
|
|
var li = jQuery(this); |
|
611
|
|
|
li.siblings().removeClass('ui-state-highlight'); |
|
612
|
|
|
|
|
613
|
|
|
var state = {}; |
|
614
|
|
|
var pref = egw.preference('favorite_' + this.dataset.id, self.appname); |
|
615
|
|
|
if(pref) |
|
616
|
|
|
{ |
|
617
|
|
|
// Extend, to prevent changing the preference by reference |
|
618
|
|
|
jQuery.extend(true, state, pref); |
|
619
|
|
|
} |
|
620
|
|
|
if(this.dataset.id != 'add') |
|
621
|
|
|
{ |
|
622
|
|
|
event.stopImmediatePropagation(); |
|
623
|
|
|
self.setState.call(self, state); |
|
624
|
|
|
return false; |
|
625
|
|
|
} |
|
626
|
|
|
}) |
|
627
|
|
|
.addClass("ui-helper-clearfix"); |
|
628
|
|
|
|
|
629
|
|
|
//Add Sortable handler to sideBox fav. menu |
|
630
|
|
|
jQuery('ul','#favorite_sidebox_'+this.appname).sortable({ |
|
631
|
|
|
items:'li:not([data-id$="add"])', |
|
632
|
|
|
placeholder:'ui-fav-sortable-placeholder', |
|
633
|
|
|
delay:250, //(millisecond) delay before the sorting should start |
|
634
|
|
|
helper: function(event, item : any) { |
|
635
|
|
|
// We'll need to know which app this is for |
|
636
|
|
|
item.attr('data-appname',self.appname); |
|
637
|
|
|
// Create custom helper so it can be dragged to Home |
|
638
|
|
|
var h_parent = item.parent().parent().clone(); |
|
639
|
|
|
h_parent.find('li').not('[data-id="'+item.attr('data-id')+'"]').remove(); |
|
640
|
|
|
h_parent.appendTo('body'); |
|
641
|
|
|
return h_parent; |
|
642
|
|
|
}, |
|
643
|
|
|
// @ts-ignore |
|
644
|
|
|
refreshPositions: true, |
|
645
|
|
|
update: function (event, ui) |
|
646
|
|
|
{ |
|
647
|
|
|
// @ts-ignore |
|
648
|
|
|
var favSortedList = jQuery(this).sortable('toArray', {attribute:'data-id'}); |
|
649
|
|
|
|
|
650
|
|
|
self.egw.set_preference(self.appname,'fav_sort_pref',favSortedList); |
|
651
|
|
|
|
|
652
|
|
|
self._refresh_fav_nm(); |
|
653
|
|
|
} |
|
654
|
|
|
}); |
|
655
|
|
|
|
|
656
|
|
|
// Bind favorite de-select |
|
657
|
|
|
var egw_fw = egw_getFramework(); |
|
658
|
|
|
if(egw_fw && egw_fw.applications[this.appname] && egw_fw.applications[this.appname].browser |
|
659
|
|
|
&& egw_fw.applications[this.appname].browser.baseDiv) |
|
660
|
|
|
{ |
|
661
|
|
|
jQuery(egw_fw.applications[this.appname].browser.baseDiv) |
|
662
|
|
|
.off('.sidebox') |
|
663
|
|
|
.on('change.sidebox', function() { |
|
664
|
|
|
self.highlight_favorite(); |
|
665
|
|
|
}); |
|
666
|
|
|
} |
|
667
|
|
|
return true; |
|
668
|
|
|
} |
|
669
|
|
|
return false; |
|
670
|
|
|
} |
|
671
|
|
|
|
|
672
|
|
|
/** |
|
673
|
|
|
* Add a new favorite |
|
674
|
|
|
* |
|
675
|
|
|
* Fetches the current state from the application, then opens a dialog to get the |
|
676
|
|
|
* name and other settings. If user proceeds, the favorite is saved, and if possible |
|
677
|
|
|
* the sidebox is directly updated to include the new favorite |
|
678
|
|
|
* |
|
679
|
|
|
* @param {object} [state] State settings to be merged into the application state |
|
680
|
|
|
*/ |
|
681
|
|
|
add_favorite(state) |
|
682
|
|
|
{ |
|
683
|
|
|
if(typeof this.favorite_popup == "undefined" || // Create popup if it's not defined yet |
|
684
|
|
|
(this.favorite_popup && typeof this.favorite_popup.group != "undefined" |
|
685
|
|
|
&& !this.favorite_popup.group.isAttached())) // recreate the favorite popup if the group selectbox is not attached (eg. after et2 submit) |
|
686
|
|
|
{ |
|
687
|
|
|
this._create_favorite_popup(); |
|
688
|
|
|
} |
|
689
|
|
|
// Get current state |
|
690
|
|
|
this.favorite_popup.state = jQuery.extend({}, this.getState(), state||{}); |
|
691
|
|
|
/* |
|
692
|
|
|
// Add in extras |
|
693
|
|
|
for(var extra in this.options.filters) |
|
694
|
|
|
{ |
|
695
|
|
|
// Don't overwrite what nm has, chances are nm has more up-to-date value |
|
696
|
|
|
if(typeof this.popup.current_filters == 'undefined') |
|
697
|
|
|
{ |
|
698
|
|
|
this.popup.current_filters[extra] = this.nextmatch.options.settings[extra]; |
|
699
|
|
|
} |
|
700
|
|
|
} |
|
701
|
|
|
|
|
702
|
|
|
// Add in application's settings |
|
703
|
|
|
if(this.filters != true) |
|
704
|
|
|
{ |
|
705
|
|
|
for(var i = 0; i < this.filters.length; i++) |
|
706
|
|
|
{ |
|
707
|
|
|
this.popup.current_filters[this.options.filters[i]] = this.nextmatch.options.settings[this.options.filters[i]]; |
|
708
|
|
|
} |
|
709
|
|
|
} |
|
710
|
|
|
*/ |
|
711
|
|
|
// Make sure it's an object - deep copy to prevent references in sub-objects (col_filters) |
|
712
|
|
|
this.favorite_popup.state = jQuery.extend(true,{},this.favorite_popup.state); |
|
713
|
|
|
|
|
714
|
|
|
// Update popup with current set filters (more for debug than user) |
|
715
|
|
|
var filter_list = []; |
|
716
|
|
|
var add_to_popup = function(arr) { |
|
717
|
|
|
filter_list.push("<ul>"); |
|
718
|
|
|
jQuery.each(arr, function(index, filter) { |
|
719
|
|
|
filter_list.push("<li id='index'><span class='filter_id'>"+index+"</span>" + |
|
720
|
|
|
(typeof filter != "object" ? "<span class='filter_value'>"+filter+"</span>": "") |
|
721
|
|
|
); |
|
722
|
|
|
if(typeof filter == "object" && filter != null) add_to_popup(filter); |
|
723
|
|
|
filter_list.push("</li>"); |
|
724
|
|
|
}); |
|
725
|
|
|
filter_list.push("</ul>"); |
|
726
|
|
|
}; |
|
727
|
|
|
add_to_popup(this.favorite_popup.state); |
|
728
|
|
|
jQuery("#"+this.appname+"_favorites_popup_state",this.favorite_popup) |
|
729
|
|
|
.replaceWith( |
|
730
|
|
|
jQuery(filter_list.join("")).attr("id",this.appname+"_favorites_popup_state") |
|
731
|
|
|
); |
|
732
|
|
|
jQuery("#"+this.appname+"_favorites_popup_state",this.favorite_popup) |
|
733
|
|
|
.hide() |
|
734
|
|
|
.siblings(".ui-icon-circle-plus") |
|
735
|
|
|
.removeClass("ui-icon-circle-minus"); |
|
736
|
|
|
|
|
737
|
|
|
// Popup |
|
738
|
|
|
this.favorite_popup.dialog("open"); |
|
739
|
|
|
console.log(this); |
|
740
|
|
|
|
|
741
|
|
|
// Stop the normal bubbling if this is called on click |
|
742
|
|
|
return false; |
|
743
|
|
|
} |
|
744
|
|
|
|
|
745
|
|
|
/** |
|
746
|
|
|
* Update favorite items in nm fav. menu |
|
747
|
|
|
* |
|
748
|
|
|
*/ |
|
749
|
|
|
_refresh_fav_nm () |
|
750
|
|
|
{ |
|
751
|
|
|
var self = this; |
|
752
|
|
|
|
|
753
|
|
|
if(etemplate2 && etemplate2.getByApplication) |
|
754
|
|
|
{ |
|
755
|
|
|
var et2 = etemplate2.getByApplication(self.appname); |
|
756
|
|
|
for(var i = 0; i < et2.length; i++) |
|
757
|
|
|
{ |
|
758
|
|
|
et2[i].widgetContainer.iterateOver(function(_widget) { |
|
759
|
|
|
_widget.stored_filters = _widget.load_favorites(self.appname); |
|
760
|
|
|
_widget.init_filters(_widget); |
|
761
|
|
|
}, self, et2_favorites); |
|
762
|
|
|
} |
|
763
|
|
|
} |
|
764
|
|
|
else |
|
765
|
|
|
{ |
|
766
|
|
|
throw new Error ("_refresh_fav_nm():Either et2 is not ready/ not there yet. Make sure that etemplate2 is ready before call this method."); |
|
767
|
|
|
} |
|
768
|
|
|
} |
|
769
|
|
|
|
|
770
|
|
|
/** |
|
771
|
|
|
* Create the "Add new" popup dialog |
|
772
|
|
|
*/ |
|
773
|
|
|
_create_favorite_popup() |
|
774
|
|
|
{ |
|
775
|
|
|
var self = this; |
|
776
|
|
|
var favorite_prefix = 'favorite_'; |
|
777
|
|
|
|
|
778
|
|
|
// Clear old, if existing |
|
779
|
|
|
if(this.favorite_popup && this.favorite_popup.group) |
|
780
|
|
|
{ |
|
781
|
|
|
this.favorite_popup.group.free(); |
|
782
|
|
|
delete this.favorite_popup; |
|
783
|
|
|
} |
|
784
|
|
|
|
|
785
|
|
|
// Create popup |
|
786
|
|
|
this.favorite_popup = jQuery('<div id="'+this.dom_id + '_nm_favorites_popup" title="' + egw().lang("New favorite") + '">\ |
|
787
|
|
|
<form>\ |
|
788
|
|
|
<label for="name">'+ |
|
789
|
|
|
this.egw.lang("name") + |
|
790
|
|
|
'</label>' + |
|
791
|
|
|
|
|
792
|
|
|
'<input type="text" name="name" id="name"/>\ |
|
793
|
|
|
<div id="'+this.appname+'_favorites_popup_admin"/>\ |
|
794
|
|
|
<span>'+ this.egw.lang("Details") + '</span><span style="float:left;" class="ui-icon ui-icon-circle-plus ui-button" />\ |
|
795
|
|
|
<ul id="'+this.appname+'_favorites_popup_state"/>\ |
|
796
|
|
|
</form>\ |
|
797
|
|
|
</div>' |
|
798
|
|
|
).appendTo(this.et2 ? this.et2.getDOMNode() : jQuery('body')); |
|
799
|
|
|
|
|
800
|
|
|
// @ts-ignore |
|
801
|
|
|
jQuery(".ui-icon-circle-plus",this.favorite_popup).prev().andSelf().click(function() { |
|
802
|
|
|
var details = jQuery("#"+self.appname+"_favorites_popup_state",self.favorite_popup) |
|
803
|
|
|
.slideToggle() |
|
804
|
|
|
.siblings(".ui-icon-circle-plus") |
|
805
|
|
|
.toggleClass("ui-icon-circle-minus"); |
|
806
|
|
|
}); |
|
807
|
|
|
|
|
808
|
|
|
// Add some controls if user is an admin |
|
809
|
|
|
var apps = egw().user('apps'); |
|
810
|
|
|
var is_admin = (typeof apps['admin'] != "undefined"); |
|
811
|
|
|
if(is_admin) |
|
812
|
|
|
{ |
|
813
|
|
|
this.favorite_popup.group = et2_createWidget("select-account",{ |
|
814
|
|
|
id: "favorite[group]", |
|
815
|
|
|
account_type: "groups", |
|
816
|
|
|
empty_label: "Groups", |
|
817
|
|
|
no_lang: true, |
|
818
|
|
|
parent_node: this.appname+'_favorites_popup_admin' |
|
819
|
|
|
},(this.et2 || null)); |
|
820
|
|
|
this.favorite_popup.group.loadingFinished(); |
|
821
|
|
|
} |
|
822
|
|
|
|
|
823
|
|
|
var buttons = {}; |
|
824
|
|
|
buttons['save'] = { |
|
825
|
|
|
text: this.egw.lang('save'), |
|
826
|
|
|
default: true, |
|
827
|
|
|
style: 'background-image: url('+this.egw.image('save')+')', |
|
828
|
|
|
click: function() { |
|
829
|
|
|
// Add a new favorite |
|
830
|
|
|
var name = jQuery("#name",this); |
|
831
|
|
|
|
|
832
|
|
|
if(name.val()) |
|
833
|
|
|
{ |
|
834
|
|
|
// Add to the list |
|
835
|
|
|
name.val(name.val().replace(/(<([^>]+)>)/ig,"")); |
|
836
|
|
|
var safe_name = name.val().replace(/[^A-Za-z0-9-_]/g,"_"); |
|
837
|
|
|
var favorite = { |
|
838
|
|
|
name: name.val(), |
|
839
|
|
|
group: (typeof self.favorite_popup.group != "undefined" && |
|
840
|
|
|
self.favorite_popup.group.get_value() ? self.favorite_popup.group.get_value() : false), |
|
841
|
|
|
state: self.favorite_popup.state |
|
842
|
|
|
}; |
|
843
|
|
|
|
|
844
|
|
|
var favorite_pref = favorite_prefix+safe_name; |
|
845
|
|
|
|
|
846
|
|
|
// Save to preferences |
|
847
|
|
|
if(typeof self.favorite_popup.group != "undefined" && self.favorite_popup.group.getValue() != '') |
|
848
|
|
|
{ |
|
849
|
|
|
// Admin stuff - save preference server side |
|
850
|
|
|
self.egw.jsonq('EGroupware\\Api\\Framework::ajax_set_favorite', |
|
851
|
|
|
[ |
|
852
|
|
|
self.appname, |
|
853
|
|
|
name.val(), |
|
854
|
|
|
"add", |
|
855
|
|
|
self.favorite_popup.group.get_value(), |
|
856
|
|
|
self.favorite_popup.state |
|
857
|
|
|
] |
|
858
|
|
|
); |
|
859
|
|
|
self.favorite_popup.group.set_value(''); |
|
860
|
|
|
} |
|
861
|
|
|
else |
|
862
|
|
|
{ |
|
863
|
|
|
// Normal user - just save to preferences client side |
|
864
|
|
|
self.egw.set_preference(self.appname,favorite_pref,favorite); |
|
865
|
|
|
} |
|
866
|
|
|
|
|
867
|
|
|
// Add to list immediately |
|
868
|
|
|
if(self.sidebox) |
|
869
|
|
|
{ |
|
870
|
|
|
// Remove any existing with that name |
|
871
|
|
|
jQuery('[data-id="'+safe_name+'"]',self.sidebox).remove(); |
|
872
|
|
|
|
|
873
|
|
|
// Create new item |
|
874
|
|
|
var html = "<li data-id='"+safe_name+"' data-group='" + favorite.group + "' class='ui-menu-item' role='menuitem'>\n"; |
|
875
|
|
|
var href = 'javascript:app.'+self.appname+'.setState('+JSON.stringify(favorite)+');'; |
|
876
|
|
|
html += "<a href='"+href+"' class='ui-corner-all' tabindex='-1'>"; |
|
877
|
|
|
html += "<div class='" + 'sideboxstar' + "'></div>"+ |
|
878
|
|
|
favorite.name; |
|
879
|
|
|
html += "<div class='ui-icon ui-icon-trash' title='" + egw.lang('Delete') + "'/>"; |
|
880
|
|
|
html += "</a></li>\n"; |
|
881
|
|
|
jQuery(html).insertBefore(jQuery('li',self.sidebox).last()); |
|
882
|
|
|
self._init_sidebox(self.sidebox); |
|
883
|
|
|
} |
|
884
|
|
|
|
|
885
|
|
|
// Try to update nextmatch favorites too |
|
886
|
|
|
self._refresh_fav_nm(); |
|
887
|
|
|
} |
|
888
|
|
|
// Reset form |
|
889
|
|
|
delete self.favorite_popup.state; |
|
890
|
|
|
name.val(""); |
|
891
|
|
|
jQuery("#filters",self.favorite_popup).empty(); |
|
892
|
|
|
|
|
893
|
|
|
jQuery(this).dialog("close"); |
|
894
|
|
|
} |
|
895
|
|
|
}; |
|
896
|
|
|
buttons['cancel'] = { |
|
897
|
|
|
text: this.egw.lang("cancel"), |
|
898
|
|
|
style: 'background-image: url('+this.egw.image('cancel')+')', |
|
899
|
|
|
click: function() { |
|
900
|
|
|
if(typeof self.favorite_popup.group !== 'undefined' && self.favorite_popup.group.set_value) |
|
901
|
|
|
{ |
|
902
|
|
|
self.favorite_popup.group.set_value(null); |
|
903
|
|
|
} |
|
904
|
|
|
jQuery(this).dialog("close"); |
|
905
|
|
|
} |
|
906
|
|
|
}; |
|
907
|
|
|
|
|
908
|
|
|
this.favorite_popup.dialog({ |
|
909
|
|
|
autoOpen: false, |
|
910
|
|
|
modal: true, |
|
911
|
|
|
buttons: buttons, |
|
912
|
|
|
close: function() { |
|
913
|
|
|
} |
|
914
|
|
|
}); |
|
915
|
|
|
|
|
916
|
|
|
// Bind handler for enter keypress |
|
917
|
|
|
this.favorite_popup.off('keydown').on('keydown', jQuery.proxy(function(e) { |
|
918
|
|
|
var tagName = e.target.tagName.toLowerCase(); |
|
919
|
|
|
tagName = (tagName === 'input' && e.target.type === 'button') ? 'button' : tagName; |
|
920
|
|
|
|
|
921
|
|
|
if(e.keyCode == jQuery.ui.keyCode.ENTER && tagName !== 'textarea' && tagName !== 'select' && tagName !=='button') |
|
922
|
|
|
{ |
|
923
|
|
|
e.preventDefault(); |
|
924
|
|
|
jQuery('button[default]',this.favorite_popup.parent()).trigger('click'); |
|
925
|
|
|
return false; |
|
926
|
|
|
} |
|
927
|
|
|
},this)); |
|
928
|
|
|
|
|
929
|
|
|
return false; |
|
930
|
|
|
} |
|
931
|
|
|
|
|
932
|
|
|
/** |
|
933
|
|
|
* Delete a favorite from the list and update preferences |
|
934
|
|
|
* Registered as a handler on the delete icons |
|
935
|
|
|
* |
|
936
|
|
|
* @param {jQuery.event} event event object |
|
937
|
|
|
*/ |
|
938
|
|
|
delete_favorite(event) |
|
939
|
|
|
{ |
|
940
|
|
|
// Don't do the menu |
|
941
|
|
|
event.stopImmediatePropagation(); |
|
942
|
|
|
|
|
943
|
|
|
var app = event.data; |
|
944
|
|
|
var id = jQuery(this).parentsUntil('li').parent().attr("data-id"); |
|
945
|
|
|
var group = jQuery(this).parentsUntil('li').parent().attr("data-group") || ''; |
|
946
|
|
|
var line = jQuery('li[data-id="'+id+'"]',app.sidebox); |
|
947
|
|
|
var name = line.first().text(); |
|
948
|
|
|
var trash = this; |
|
949
|
|
|
line.addClass('loading'); |
|
950
|
|
|
|
|
951
|
|
|
// Make sure first |
|
952
|
|
|
var do_delete = function(button_id) |
|
953
|
|
|
{ |
|
954
|
|
|
if(button_id != et2_dialog.YES_BUTTON) |
|
955
|
|
|
{ |
|
956
|
|
|
line.removeClass('loading'); |
|
957
|
|
|
return; |
|
958
|
|
|
} |
|
959
|
|
|
|
|
960
|
|
|
// Hide the trash |
|
961
|
|
|
jQuery(trash).hide(); |
|
962
|
|
|
|
|
963
|
|
|
// Delete preference server side |
|
964
|
|
|
var request = egw.json("EGroupware\\Api\\Framework::ajax_set_favorite", |
|
965
|
|
|
[app.appname, id, "delete", group, ''], |
|
966
|
|
|
function(result) { |
|
967
|
|
|
// Got the full response from callback, which we don't want |
|
968
|
|
|
if(result.type) return; |
|
969
|
|
|
|
|
970
|
|
|
if(result && typeof result == 'boolean') |
|
971
|
|
|
{ |
|
972
|
|
|
// Remove line from list |
|
973
|
|
|
line.slideUp("slow", function() { }); |
|
974
|
|
|
|
|
975
|
|
|
app._refresh_fav_nm(); |
|
976
|
|
|
} |
|
977
|
|
|
else |
|
978
|
|
|
{ |
|
979
|
|
|
// Something went wrong server side |
|
980
|
|
|
line.removeClass('loading').addClass('ui-state-error'); |
|
981
|
|
|
} |
|
982
|
|
|
}, |
|
983
|
|
|
jQuery(trash).parentsUntil("li").parent(), |
|
984
|
|
|
true, |
|
985
|
|
|
jQuery(trash).parentsUntil("li").parent() |
|
986
|
|
|
); |
|
987
|
|
|
request.sendRequest(true); |
|
988
|
|
|
}; |
|
989
|
|
|
et2_dialog.show_dialog(do_delete, (egw.lang("Delete") + " " +name +"?"), |
|
990
|
|
|
egw.lang("Delete"), et2_dialog.YES_NO, et2_dialog.QUESTION_MESSAGE); |
|
991
|
|
|
|
|
992
|
|
|
return false; |
|
993
|
|
|
} |
|
994
|
|
|
|
|
995
|
|
|
/** |
|
996
|
|
|
* Mark the favorite closest matching the current state |
|
997
|
|
|
* |
|
998
|
|
|
* Closest matching takes into account not set values, so we pick the favorite |
|
999
|
|
|
* with the most matching values without a value that differs. |
|
1000
|
|
|
*/ |
|
1001
|
|
|
highlight_favorite() { |
|
1002
|
|
|
if(!this.sidebox) return; |
|
1003
|
|
|
|
|
1004
|
|
|
var state = this.getState(); |
|
1005
|
|
|
var best_match = false; |
|
1006
|
|
|
var best_count = 0; |
|
1007
|
|
|
var self = this; |
|
1008
|
|
|
|
|
1009
|
|
|
jQuery('li[data-id]',this.sidebox).removeClass('ui-state-highlight'); |
|
1010
|
|
|
|
|
1011
|
|
|
jQuery('li[data-id]',this.sidebox).each(function(i,href) { |
|
1012
|
|
|
var favorite : any = {}; |
|
1013
|
|
|
if(this.dataset.id && egw.preference('favorite_'+this.dataset.id,self.appname)) |
|
1014
|
|
|
{ |
|
1015
|
|
|
favorite = egw.preference('favorite_'+this.dataset.id,self.appname); |
|
1016
|
|
|
} |
|
1017
|
|
|
if(!favorite || jQuery.isEmptyObject(favorite)) return; |
|
1018
|
|
|
|
|
1019
|
|
|
// Handle old style by making it like new style |
|
1020
|
|
|
if(favorite.filter && !favorite.state) |
|
1021
|
|
|
{ |
|
1022
|
|
|
favorite.state = favorite.filter; |
|
1023
|
|
|
} |
|
1024
|
|
|
|
|
1025
|
|
|
var match_count = 0; |
|
1026
|
|
|
var extra_keys = Object.keys(favorite.state); |
|
1027
|
|
|
for(var state_key in state) |
|
1028
|
|
|
{ |
|
1029
|
|
|
extra_keys.splice(extra_keys.indexOf(state_key),1); |
|
1030
|
|
|
if(typeof favorite.state != 'undefined' && typeof state[state_key] != 'undefined'&&typeof favorite.state[state_key] != 'undefined' && ( state[state_key] == favorite.state[state_key] || !state[state_key] && !favorite.state[state_key])) |
|
1031
|
|
|
{ |
|
1032
|
|
|
match_count++; |
|
1033
|
|
|
} |
|
1034
|
|
|
else if (state_key == 'selectcols') |
|
1035
|
|
|
{ |
|
1036
|
|
|
// Skip, might be set, might not |
|
1037
|
|
|
} |
|
1038
|
|
|
else if (typeof state[state_key] != 'undefined' && state[state_key] && typeof state[state_key] === 'object' |
|
1039
|
|
|
&& typeof favorite.state != 'undefined' && typeof favorite.state[state_key] != 'undefined' && favorite.state[state_key] && typeof favorite.state[state_key] === 'object') |
|
1040
|
|
|
{ |
|
1041
|
|
|
if((typeof state[state_key].length !== 'undefined' || typeof state[state_key].length !== 'undefined') |
|
1042
|
|
|
&& (state[state_key].length || Object.keys(state[state_key]).length) != (favorite.state[state_key].length || Object.keys(favorite.state[state_key]).length )) |
|
1043
|
|
|
{ |
|
1044
|
|
|
// State or favorite has a length, but the other does not |
|
1045
|
|
|
if((state[state_key].length === 0 || Object.keys(state[state_key]).length === 0) && |
|
1046
|
|
|
(favorite.state[state_key].length == 0 || Object.keys(favorite.state[state_key]).length === 0)) |
|
1047
|
|
|
{ |
|
1048
|
|
|
// Just missing, or one is an array and the other is an object |
|
1049
|
|
|
continue; |
|
1050
|
|
|
} |
|
1051
|
|
|
// One has a value and the other doesn't, no match |
|
1052
|
|
|
return; |
|
1053
|
|
|
} |
|
1054
|
|
|
else if (state[state_key].length !== 'undefined' && typeof favorite.state[state_key].length !== 'undefined' && |
|
1055
|
|
|
state[state_key].length === 0 && favorite.state[state_key].length === 0) |
|
1056
|
|
|
{ |
|
1057
|
|
|
// Both set, but both empty |
|
1058
|
|
|
match_count++; |
|
1059
|
|
|
continue; |
|
1060
|
|
|
} |
|
1061
|
|
|
// Consider sub-objects (column filters) individually |
|
1062
|
|
|
for(var sub_key in state[state_key]) |
|
1063
|
|
|
{ |
|
1064
|
|
|
if(state[state_key][sub_key] == favorite.state[state_key][sub_key] || !state[state_key][sub_key] && !favorite.state[state_key][sub_key]) |
|
1065
|
|
|
{ |
|
1066
|
|
|
match_count++; |
|
1067
|
|
|
} |
|
1068
|
|
|
else if (state[state_key][sub_key] && favorite.state[state_key][sub_key] && |
|
1069
|
|
|
typeof state[state_key][sub_key] === 'object' && typeof favorite.state[state_key][sub_key] === 'object') |
|
1070
|
|
|
{ |
|
1071
|
|
|
// Too deep to keep going, just string compare for perfect match |
|
1072
|
|
|
if(JSON.stringify(state[state_key][sub_key]) === JSON.stringify(favorite.state[state_key][sub_key])) |
|
1073
|
|
|
{ |
|
1074
|
|
|
match_count++; |
|
1075
|
|
|
} |
|
1076
|
|
|
} |
|
1077
|
|
|
else if(typeof state[state_key][sub_key] !== 'undefined' && state[state_key][sub_key] != favorite.state[state_key][sub_key]) |
|
1078
|
|
|
{ |
|
1079
|
|
|
// Different values, do not match |
|
1080
|
|
|
return; |
|
1081
|
|
|
} |
|
1082
|
|
|
} |
|
1083
|
|
|
} |
|
1084
|
|
|
else if (typeof state[state_key] !== 'undefined' |
|
1085
|
|
|
&& typeof favorite.state != 'undefined'&&typeof favorite.state[state_key] !== 'undefined' |
|
1086
|
|
|
&& state[state_key] != favorite.state[state_key]) |
|
1087
|
|
|
{ |
|
1088
|
|
|
// Different values, do not match |
|
1089
|
|
|
return; |
|
1090
|
|
|
} |
|
1091
|
|
|
} |
|
1092
|
|
|
// Check for anything set that the current one does not have |
|
1093
|
|
|
for(var i = 0; i < extra_keys.length; i++) |
|
1094
|
|
|
{ |
|
1095
|
|
|
if(favorite.state[extra_keys[i]]) return; |
|
1096
|
|
|
} |
|
1097
|
|
|
if(match_count > best_count) |
|
1098
|
|
|
{ |
|
1099
|
|
|
best_match = this.dataset.id; |
|
1100
|
|
|
best_count = match_count; |
|
1101
|
|
|
} |
|
1102
|
|
|
}); |
|
1103
|
|
|
if(best_match) |
|
1104
|
|
|
{ |
|
1105
|
|
|
jQuery('li[data-id="'+best_match+'"]',this.sidebox).addClass('ui-state-highlight'); |
|
1106
|
|
|
} |
|
1107
|
|
|
} |
|
1108
|
|
|
|
|
1109
|
|
|
/** |
|
1110
|
|
|
* Fix scrolling iframe browsed by iPhone/iPod/iPad touch devices |
|
1111
|
|
|
*/ |
|
1112
|
|
|
_fix_iFrameScrolling() |
|
1113
|
|
|
{ |
|
1114
|
|
|
if (/iPhone|iPod|iPad/.test(navigator.userAgent)) |
|
1115
|
|
|
{ |
|
1116
|
|
|
jQuery("iframe").on({ |
|
1117
|
|
|
load: function() |
|
1118
|
|
|
{ |
|
1119
|
|
|
var body = this.contentWindow.document.body; |
|
1120
|
|
|
|
|
1121
|
|
|
var div = jQuery(document.createElement("div")) |
|
1122
|
|
|
.css ({ |
|
1123
|
|
|
'height' : jQuery(this.parentNode).height(), |
|
1124
|
|
|
'width' : jQuery(this.parentNode).width(), |
|
1125
|
|
|
'overflow' : 'scroll'}); |
|
1126
|
|
|
while (body.firstChild) |
|
1127
|
|
|
{ |
|
1128
|
|
|
div.append(body.firstChild); |
|
1129
|
|
|
} |
|
1130
|
|
|
jQuery(body).append(div); |
|
1131
|
|
|
} |
|
1132
|
|
|
}); |
|
1133
|
|
|
} |
|
1134
|
|
|
} |
|
1135
|
|
|
|
|
1136
|
|
|
/** |
|
1137
|
|
|
* Set document title, uses getWindowTitle to get the correct title, |
|
1138
|
|
|
* otherwise set it with uniqueID as default title |
|
1139
|
|
|
*/ |
|
1140
|
|
|
_set_Window_title () |
|
1141
|
|
|
{ |
|
1142
|
|
|
var title = this.getWindowTitle(); |
|
1143
|
|
|
if (title) |
|
1144
|
|
|
{ |
|
1145
|
|
|
document.title = this.et2._inst.uniqueId + ": " + title; |
|
1146
|
|
|
} |
|
1147
|
|
|
} |
|
1148
|
|
|
|
|
1149
|
|
|
/** |
|
1150
|
|
|
* Window title getter function in order to set the window title |
|
1151
|
|
|
* this can be overridden on each application app.js file to customize the title value |
|
1152
|
|
|
* |
|
1153
|
|
|
* @returns {string} window title |
|
1154
|
|
|
*/ |
|
1155
|
|
|
getWindowTitle () |
|
1156
|
|
|
{ |
|
1157
|
|
|
var titleWidget = this.et2.getWidgetById('title'); |
|
1158
|
|
|
if (titleWidget) |
|
1159
|
|
|
{ |
|
1160
|
|
|
return titleWidget.options.value; |
|
1161
|
|
|
} |
|
1162
|
|
|
else |
|
1163
|
|
|
{ |
|
1164
|
|
|
return this.et2._inst.uniqueId; |
|
1165
|
|
|
} |
|
1166
|
|
|
} |
|
1167
|
|
|
|
|
1168
|
|
|
/** |
|
1169
|
|
|
* Handler for drag and drop when dragging nextmatch rows from mail app |
|
1170
|
|
|
* and dropped on a row in the current application. We copy the mail into |
|
1171
|
|
|
* the filemanager to link it since we can't link directly. |
|
1172
|
|
|
* |
|
1173
|
|
|
* This doesn't happen automatically. Each application must indicate that |
|
1174
|
|
|
* it will accept dropped mail by it's nextmatch actions: |
|
1175
|
|
|
* |
|
1176
|
|
|
* $actions['info_drop_mail'] = array( |
|
1177
|
|
|
* 'type' => 'drop', |
|
1178
|
|
|
* 'acceptedTypes' => 'mail', |
|
1179
|
|
|
* 'onExecute' => 'javaScript:app.infolog.handle_dropped_mail', |
|
1180
|
|
|
* 'hideOnDisabled' => true |
|
1181
|
|
|
* ); |
|
1182
|
|
|
* |
|
1183
|
|
|
* This action, when defined, will not affect the automatic linking between |
|
1184
|
|
|
* normal applications. |
|
1185
|
|
|
* |
|
1186
|
|
|
* @param {egwAction} _action |
|
1187
|
|
|
* @param {egwActionObject[]} _selected Dragged mail rows |
|
1188
|
|
|
* @param {egwActionObject} _target Current application's nextmatch row the mail was dropped on |
|
1189
|
|
|
*/ |
|
1190
|
|
|
handle_dropped_mail(_action, _selected, _target) |
|
1191
|
|
|
{ |
|
1192
|
|
|
/** |
|
1193
|
|
|
* Mail doesn't support link system, so we copy it to VFS |
|
1194
|
|
|
*/ |
|
1195
|
|
|
var ids = _target.id.split("::"); |
|
1196
|
|
|
if(ids.length != 2 || ids[0] == 'mail') return; |
|
1197
|
|
|
|
|
1198
|
|
|
var vfs_path = "/apps/"+ids[0]+"/"+ids[1]; |
|
1199
|
|
|
var mail_ids = []; |
|
1200
|
|
|
|
|
1201
|
|
|
for(var i = 0; i < _selected.length; i++) |
|
1202
|
|
|
{ |
|
1203
|
|
|
mail_ids.push(_selected[i].id); |
|
1204
|
|
|
} |
|
1205
|
|
|
if(mail_ids.length) |
|
1206
|
|
|
{ |
|
1207
|
|
|
egw.message(egw.lang("Please wait...")); |
|
1208
|
|
|
this.egw.json('filemanager.filemanager_ui.ajax_action',['mail',mail_ids, vfs_path],function(data){ |
|
1209
|
|
|
// Trigger an update (minimal, no sorting changes) to display the new link |
|
1210
|
|
|
egw.refresh(data.msg||'',ids[0],ids[1],'update'); |
|
1211
|
|
|
}).sendRequest(true); |
|
1212
|
|
|
} |
|
1213
|
|
|
} |
|
1214
|
|
|
|
|
1215
|
|
|
/** |
|
1216
|
|
|
* Get json data for videos from the given url |
|
1217
|
|
|
* |
|
1218
|
|
|
* @return {Promise, object} return Promise, json object as resolved result and error message in case of failure |
|
1219
|
|
|
*/ |
|
1220
|
|
|
egwTutorialGetData(){ |
|
1221
|
|
|
var self = this; |
|
1222
|
|
|
return new Promise (function(_resolve, _reject) |
|
1223
|
|
|
{ |
|
1224
|
|
|
var resolve = _resolve; |
|
1225
|
|
|
var reject = _reject; |
|
1226
|
|
|
// delay the execution and let the rendering catches up. Seems only FF problem |
|
1227
|
|
|
window.setTimeout(function(){ |
|
1228
|
|
|
self.egw.json('EGroupware\\Api\\Framework\\Tutorial::ajax_data', [self.egw.app_name()], function(_data){ |
|
1229
|
|
|
resolve(_data); |
|
1230
|
|
|
}).sendRequest(); |
|
1231
|
|
|
},0); |
|
1232
|
|
|
|
|
1233
|
|
|
}); |
|
1234
|
|
|
} |
|
1235
|
|
|
|
|
1236
|
|
|
/** |
|
1237
|
|
|
* Create and Render etemplate2 for egroupware tutorial |
|
1238
|
|
|
* sidebox option. The .xet file is stored in api/templates/default/egw_tutorials |
|
1239
|
|
|
* |
|
1240
|
|
|
* @description tutorials json object should have the following structure: |
|
1241
|
|
|
* object: |
|
1242
|
|
|
* { |
|
1243
|
|
|
* [app name]:{ |
|
1244
|
|
|
* [language tag]:[ |
|
1245
|
|
|
* {src:"",thumbnail:"",title:"",desc:""} |
|
1246
|
|
|
* ] |
|
1247
|
|
|
* } |
|
1248
|
|
|
* } |
|
1249
|
|
|
* |
|
1250
|
|
|
* *Note: "desc" and "title" are optional attributes, which "desc" would appears as tooltip for the video. |
|
1251
|
|
|
* |
|
1252
|
|
|
* example: |
|
1253
|
|
|
* { |
|
1254
|
|
|
* "mail":{ |
|
1255
|
|
|
* "en":[ |
|
1256
|
|
|
* {src:"https://www.youtube.com/embed/mCDJndpjO40", thumbnail:"http://img.youtube.com/vi/mCDJndpjO40/0.jpg", "title":"PGP Encryption", "desc":""}, |
|
1257
|
|
|
* {src:"https://www.youtube.com/embed/mCDJndpjO", thumbnail:"http://img.youtube.com/vi/mCDJndpjO/0.jpg", "title":"Subscription", "desc":""}, |
|
1258
|
|
|
* ], |
|
1259
|
|
|
* "de":[ |
|
1260
|
|
|
* {src:"https://www.youtube.com/embed/m40", thumbnail:"http://img.youtube.com/vi/m40/0.jpg", "title":"PGP Verschlüsselung", "desc":""}, |
|
1261
|
|
|
* {src:"https://www.youtube.com/embed/mpjO", thumbnail:"http://img.youtube.com/vi/mpjO/0.jpg", "title":"Ordner Abonnieren", "desc":""}, |
|
1262
|
|
|
* ] |
|
1263
|
|
|
* } |
|
1264
|
|
|
* } |
|
1265
|
|
|
* |
|
1266
|
|
|
* @param {DOMNode} div |
|
1267
|
|
|
*/ |
|
1268
|
|
|
egwTutorial_init(div) |
|
1269
|
|
|
{ |
|
1270
|
|
|
// et2 object |
|
1271
|
|
|
var etemplate = new etemplate2 (div, false); |
|
1272
|
|
|
var template = egw.webserverUrl+'/api/templates/default/egw_tutorial.xet?1'; |
|
1273
|
|
|
|
|
1274
|
|
|
this.egwTutorialGetData().then(function(_data){ |
|
1275
|
|
|
var lang = egw.preference('lang'); |
|
1276
|
|
|
var content = {content:{list:[]}}; |
|
1277
|
|
|
if (_data && _data[egw.app_name()]) |
|
1278
|
|
|
{ |
|
1279
|
|
|
if (!_data[egw.app_name()][lang]) lang = 'en'; |
|
1280
|
|
|
if (typeof _data[egw.app_name()][lang] !='undefined' |
|
1281
|
|
|
&& _data[egw.app_name()][lang].length > 0) |
|
1282
|
|
|
{ |
|
1283
|
|
|
for (var i=0;i < _data[egw.app_name()][lang].length;i++) |
|
1284
|
|
|
{ |
|
1285
|
|
|
var tuid = egw.app_name() + '-' +lang + '-' + i; |
|
1286
|
|
|
_data[egw.app_name()][lang][i]['onclick'] = 'app.'+egw.app_name()+'.egwTutorialPopup("'+tuid+'")'; |
|
1287
|
|
|
} |
|
1288
|
|
|
content.content.list = _data[egw.app_name()][lang]; |
|
1289
|
|
|
|
|
1290
|
|
|
if (template.indexOf('.xet') >0) |
|
1291
|
|
|
{ |
|
1292
|
|
|
etemplate.load ('',template , content, function(){}); |
|
1293
|
|
|
} |
|
1294
|
|
|
else |
|
1295
|
|
|
{ |
|
1296
|
|
|
etemplate.load (template, '', content); |
|
1297
|
|
|
} |
|
1298
|
|
|
} |
|
1299
|
|
|
} |
|
1300
|
|
|
}, |
|
1301
|
|
|
function(_err){ |
|
1302
|
|
|
console.log(_err); |
|
1303
|
|
|
}); |
|
1304
|
|
|
} |
|
1305
|
|
|
|
|
1306
|
|
|
/** |
|
1307
|
|
|
* Open popup to show given tutorial id |
|
1308
|
|
|
* @param {string} _tuid tutorial object id |
|
1309
|
|
|
* - tuid: appname-lang-index |
|
1310
|
|
|
*/ |
|
1311
|
|
|
egwTutorialPopup (_tuid) |
|
1312
|
|
|
{ |
|
1313
|
|
|
var url = egw.link('/index.php', 'menuaction=api.EGroupware\\Api\\Framework\\Tutorial.popup&tuid='+_tuid); |
|
1314
|
|
|
egw.open_link(url,'_blank','960x580'); |
|
1315
|
|
|
} |
|
1316
|
|
|
|
|
1317
|
|
|
/** |
|
1318
|
|
|
* Function to set video iframe base on selected tutorial from tutorials box |
|
1319
|
|
|
* |
|
1320
|
|
|
* @param {string} _url |
|
1321
|
|
|
*/ |
|
1322
|
|
|
tutorial_videoOnClick (_url) |
|
1323
|
|
|
{ |
|
1324
|
|
|
var frame = etemplate2.getByApplication('api')[0].widgetContainer.getWidgetById('src'); |
|
1325
|
|
|
if (frame) |
|
1326
|
|
|
{ |
|
1327
|
|
|
frame.set_value(_url); |
|
1328
|
|
|
} |
|
1329
|
|
|
} |
|
1330
|
|
|
|
|
1331
|
|
|
/** |
|
1332
|
|
|
* Function calls on discard checkbox and will set |
|
1333
|
|
|
* the egw_tutorial_noautoload preference |
|
1334
|
|
|
* |
|
1335
|
|
|
* @param {type} egw |
|
1336
|
|
|
* @param {type} widget |
|
1337
|
|
|
*/ |
|
1338
|
|
|
tutorial_autoloadDiscard (egw, widget) |
|
1339
|
|
|
{ |
|
1340
|
|
|
if (widget) |
|
1341
|
|
|
{ |
|
1342
|
|
|
this.egw.set_preference('common', 'egw_tutorial_noautoload', widget.get_value()); |
|
1343
|
|
|
} |
|
1344
|
|
|
} |
|
1345
|
|
|
|
|
1346
|
|
|
/** |
|
1347
|
|
|
* Check if Mailvelope is available, open (or create) "egroupware" keyring and call callback with it |
|
1348
|
|
|
* |
|
1349
|
|
|
* @param {function} _callback called if and only if mailvelope is available (context is this!) |
|
1350
|
|
|
*/ |
|
1351
|
|
|
mailvelopeAvailable(_callback) |
|
1352
|
|
|
{ |
|
1353
|
|
|
var self = this; |
|
1354
|
|
|
var callback = jQuery.proxy(_callback, this); |
|
1355
|
|
|
|
|
1356
|
|
|
if (typeof mailvelope !== 'undefined') |
|
1357
|
|
|
{ |
|
1358
|
|
|
this.mailvelopeOpenKeyring().then(callback); |
|
1359
|
|
|
} |
|
1360
|
|
|
else |
|
1361
|
|
|
{ |
|
1362
|
|
|
jQuery(window).on('mailvelope', function() |
|
1363
|
|
|
{ |
|
1364
|
|
|
self.mailvelopeOpenKeyring().then(callback); |
|
1365
|
|
|
}); |
|
1366
|
|
|
} |
|
1367
|
|
|
} |
|
1368
|
|
|
|
|
1369
|
|
|
/** |
|
1370
|
|
|
* mailvelope object contains SyncHandlers |
|
1371
|
|
|
* |
|
1372
|
|
|
* @property {function} descriptionuploadSync function called by Mailvelope to upload encrypted private key backup |
|
1373
|
|
|
* @property {function} downloadSync function called by Mailvelope to download encrypted private key backup |
|
1374
|
|
|
* @property {function} backup function called by Mailvelope to upload a public keyring backup |
|
1375
|
|
|
* @property {function} restore function called by Mailvelope to restore a public keyring backup |
|
1376
|
|
|
*/ |
|
1377
|
|
|
private mailvelopeSyncHandler() |
|
1378
|
|
|
{ |
|
1379
|
|
|
return { |
|
1380
|
|
|
/** |
|
1381
|
|
|
* function called by Mailvelope to upload a public keyring |
|
1382
|
|
|
* @param {UploadSyncHandler} _uploadObj |
|
1383
|
|
|
* @property {string} etag entity tag for the uploaded encrypted keyring, or null if initial upload |
|
1384
|
|
|
* @property {AsciiArmored} keyringMsg encrypted keyring as PGP armored message |
|
1385
|
|
|
* @returns {Promise.<UploadSyncReply, Error>} |
|
1386
|
|
|
*/ |
|
1387
|
|
|
uploadSync: function(_uploadObj) |
|
1388
|
|
|
{ |
|
1389
|
|
|
return new Promise(function(_resolve,_reject){}); |
|
1390
|
|
|
}, |
|
1391
|
|
|
|
|
1392
|
|
|
/** |
|
1393
|
|
|
* function called by Mailvelope to download a public keyring |
|
1394
|
|
|
* |
|
1395
|
|
|
* @param {object} _downloadObj |
|
1396
|
|
|
* @property {string} etag entity tag for the current local keyring, or null if no local eTag |
|
1397
|
|
|
* @returns {Promise.<DownloadSyncReply, Error>} |
|
1398
|
|
|
*/ |
|
1399
|
|
|
downloadSync: function(_downloadObj) |
|
1400
|
|
|
{ |
|
1401
|
|
|
return new Promise(function(_resolve,_reject){}); |
|
1402
|
|
|
}, |
|
1403
|
|
|
|
|
1404
|
|
|
/** |
|
1405
|
|
|
* function called by Mailvelope to upload an encrypted private key backup |
|
1406
|
|
|
* |
|
1407
|
|
|
* @param {BackupSyncPacket} _backup |
|
1408
|
|
|
* @property {AsciiArmored} backup an encrypted private key as PGP armored message |
|
1409
|
|
|
* @returns {Promise.<undefined, Error>} |
|
1410
|
|
|
*/ |
|
1411
|
|
|
backup: function(_backup) |
|
1412
|
|
|
{ |
|
1413
|
|
|
return new Promise(function(_resolve,_reject){ |
|
1414
|
|
|
// Store backup sync packet into .PGP-Key-Backup file in user directory |
|
1415
|
|
|
jQuery.ajax({ |
|
1416
|
|
|
method:'PUT', |
|
1417
|
|
|
url: egw.webserverUrl+'/webdav.php/home/'+egw.user('account_lid')+'/.PGP-Key-Backup', |
|
1418
|
|
|
contentType: 'application/json', |
|
1419
|
|
|
data: JSON.stringify(_backup), |
|
1420
|
|
|
success:function(){ |
|
1421
|
|
|
_resolve(_backup); |
|
1422
|
|
|
}, |
|
1423
|
|
|
error: function(_err){ |
|
1424
|
|
|
_reject(_err); |
|
1425
|
|
|
} |
|
1426
|
|
|
}); |
|
1427
|
|
|
}); |
|
1428
|
|
|
}, |
|
1429
|
|
|
|
|
1430
|
|
|
/** |
|
1431
|
|
|
* function called by Mailvelope to restore an encrypted private key backup |
|
1432
|
|
|
* |
|
1433
|
|
|
* @returns {Promise.<BackupSyncPacket, Error>} |
|
1434
|
|
|
* @todo |
|
1435
|
|
|
*/ |
|
1436
|
|
|
restore: function() |
|
1437
|
|
|
{ |
|
1438
|
|
|
return new Promise(function(_resolve,_reject){ |
|
1439
|
|
|
var resolve = _resolve; |
|
1440
|
|
|
var reject = _reject; |
|
1441
|
|
|
jQuery.ajax({ |
|
1442
|
|
|
url:egw.webserverUrl+'/webdav.php/home/'+egw.user('account_lid')+'/.PGP-Key-Backup', |
|
1443
|
|
|
method: 'GET', |
|
1444
|
|
|
success: function(_backup){ |
|
1445
|
|
|
resolve(JSON.parse(_backup)); |
|
1446
|
|
|
egw.message('Your key has been restored successfully.'); |
|
1447
|
|
|
}, |
|
1448
|
|
|
error: function(_err){ |
|
1449
|
|
|
//Try with old back file name |
|
1450
|
|
|
if (_err.status == 404) |
|
1451
|
|
|
{ |
|
1452
|
|
|
jQuery.ajax({ |
|
1453
|
|
|
method:'GET', |
|
1454
|
|
|
url: egw.webserverUrl+'/webdav.php/home/'+egw.user('account_lid')+'/.PK_PGP', |
|
1455
|
|
|
success: function(_backup){ |
|
1456
|
|
|
resolve(JSON.parse(_backup)); |
|
1457
|
|
|
egw.message('Your key has been restored successfully.'); |
|
1458
|
|
|
}, |
|
1459
|
|
|
error: function(_err){ |
|
1460
|
|
|
_reject(_err); |
|
1461
|
|
|
} |
|
1462
|
|
|
}); |
|
1463
|
|
|
} |
|
1464
|
|
|
else |
|
1465
|
|
|
{ |
|
1466
|
|
|
_reject(_err); |
|
1467
|
|
|
} |
|
1468
|
|
|
} |
|
1469
|
|
|
}); |
|
1470
|
|
|
}); |
|
1471
|
|
|
} |
|
1472
|
|
|
}; |
|
1473
|
|
|
} |
|
1474
|
|
|
|
|
1475
|
|
|
/** |
|
1476
|
|
|
* Function for backup file operations |
|
1477
|
|
|
* |
|
1478
|
|
|
* @param {type} _url Url of the backup file |
|
1479
|
|
|
* @param {type} _cmd command to operate |
|
1480
|
|
|
* - PUT: to store backup file |
|
1481
|
|
|
* - GET: to read backup file |
|
1482
|
|
|
* - DELETE: to delete backup file |
|
1483
|
|
|
* |
|
1484
|
|
|
* @param {type} _successCallback function called when the operation is successful |
|
1485
|
|
|
* @param {type} _errorCallback function called when the operation fails |
|
1486
|
|
|
* @param {type} _data data which needs to be stored in file via PUT command |
|
1487
|
|
|
*/ |
|
1488
|
|
|
_mailvelopeBackupFileOperator(_url, _cmd, _successCallback, _errorCallback, _data?) |
|
1489
|
|
|
{ |
|
1490
|
|
|
var ajaxObj = { |
|
1491
|
|
|
url: _url || egw.webserverUrl+'/webdav.php/home/'+egw.user('account_lid')+'/.PGP-Key-Backup', |
|
1492
|
|
|
method: _cmd, |
|
1493
|
|
|
success: _successCallback, |
|
1494
|
|
|
error: _errorCallback |
|
1495
|
|
|
}; |
|
1496
|
|
|
switch (_cmd) |
|
1497
|
|
|
{ |
|
1498
|
|
|
case 'PUT': |
|
1499
|
|
|
jQuery.extend({},ajaxObj, { |
|
1500
|
|
|
data: JSON.stringify(_data), |
|
1501
|
|
|
contentType: 'application/json' |
|
1502
|
|
|
}); |
|
1503
|
|
|
break; |
|
1504
|
|
|
case 'GET': |
|
1505
|
|
|
jQuery.extend({},ajaxObj, { |
|
1506
|
|
|
dataType: 'json' |
|
1507
|
|
|
}); |
|
1508
|
|
|
break; |
|
1509
|
|
|
case 'DELETE': |
|
1510
|
|
|
break; |
|
1511
|
|
|
} |
|
1512
|
|
|
jQuery.ajax(ajaxObj); |
|
1513
|
|
|
} |
|
1514
|
|
|
|
|
1515
|
|
|
/** |
|
1516
|
|
|
* Create backup dialog |
|
1517
|
|
|
* @param {string} _selector DOM selector to attach backupDialog |
|
1518
|
|
|
* @param {boolean} _initSetup determine wheter it's an initialization backup or restore backup |
|
1519
|
|
|
* |
|
1520
|
|
|
* @returns {Promise.<backupPopupId, Error>} |
|
1521
|
|
|
*/ |
|
1522
|
|
|
mailvelopeCreateBackupDialog(_selector?, _initSetup?) |
|
1523
|
|
|
{ |
|
1524
|
|
|
var self = this; |
|
1525
|
|
|
var selector = _selector || 'body'; |
|
1526
|
|
|
var initSetup = _initSetup; |
|
1527
|
|
|
jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]').remove(); |
|
1528
|
|
|
return new Promise(function(_resolve, _reject) |
|
1529
|
|
|
{ |
|
1530
|
|
|
var resolve = _resolve; |
|
1531
|
|
|
var reject = _reject; |
|
1532
|
|
|
|
|
1533
|
|
|
mailvelope.getKeyring('egroupware').then(function(_keyring : any) |
|
1534
|
|
|
{ |
|
1535
|
|
|
_keyring.addSyncHandler(self.mailvelopeSyncHandlerObj); |
|
1536
|
|
|
|
|
1537
|
|
|
var options = { |
|
1538
|
|
|
initialSetup:initSetup |
|
1539
|
|
|
}; |
|
1540
|
|
|
_keyring.createKeyBackupContainer(selector, options).then(function(_popupId){ |
|
1541
|
|
|
var $backup_selector = jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]'); |
|
1542
|
|
|
$backup_selector.css({position:'absolute', "z-index":1}); |
|
1543
|
|
|
_popupId.isReady().then(function(result){ |
|
1544
|
|
|
egw.message('Your key has been backedup into .PGP-Key-Backup successfully.'); |
|
1545
|
|
|
jQuery(selector).empty(); |
|
1546
|
|
|
}); |
|
1547
|
|
|
resolve(_popupId); |
|
1548
|
|
|
}, |
|
1549
|
|
|
function(_err){ |
|
1550
|
|
|
reject(_err); |
|
1551
|
|
|
}); |
|
1552
|
|
|
}, |
|
1553
|
|
|
function(_err) |
|
1554
|
|
|
{ |
|
1555
|
|
|
reject(_err); |
|
1556
|
|
|
}); |
|
1557
|
|
|
}); |
|
1558
|
|
|
} |
|
1559
|
|
|
|
|
1560
|
|
|
/** |
|
1561
|
|
|
* Delete backup key from filesystem |
|
1562
|
|
|
*/ |
|
1563
|
|
|
mailvelopeDeleteBackup() |
|
1564
|
|
|
{ |
|
1565
|
|
|
var self = this; |
|
1566
|
|
|
et2_dialog.show_dialog(function (_button_id) |
|
1567
|
|
|
{ |
|
1568
|
|
|
if (_button_id == et2_dialog.YES_BUTTON ) |
|
1569
|
|
|
{ |
|
1570
|
|
|
self._mailvelopeBackupFileOperator(undefined, 'DELETE', function(){ |
|
1571
|
|
|
self.egw.message(self.egw.lang('The backup key has been deleted.')); |
|
1572
|
|
|
}, function(_err){ |
|
1573
|
|
|
self.egw.message(self.egw.lang('Was not able to delete the backup key because %1',_err)); |
|
1574
|
|
|
}); |
|
1575
|
|
|
} |
|
1576
|
|
|
}, |
|
1577
|
|
|
self.egw.lang('Are you sure, you would like to delete the backup key?'), |
|
1578
|
|
|
self.egw.lang('Delete backup key'), |
|
1579
|
|
|
{}, et2_dialog.BUTTONS_YES_CANCEL, et2_dialog.QUESTION_MESSAGE, undefined, self.egw); |
|
1580
|
|
|
} |
|
1581
|
|
|
|
|
1582
|
|
|
/** |
|
1583
|
|
|
* Create mailvelope restore dialog |
|
1584
|
|
|
* @param {string} _selector DOM selector to attach restorDialog |
|
1585
|
|
|
* @param {boolean} _restorePassword if true, will restore key password too |
|
1586
|
|
|
* |
|
1587
|
|
|
* @returns {Promise} |
|
1588
|
|
|
*/ |
|
1589
|
|
|
mailvelopeCreateRestoreDialog(_selector, _restorePassword) |
|
1590
|
|
|
{ |
|
1591
|
|
|
var self = this; |
|
1592
|
|
|
var restorePassword = _restorePassword; |
|
1593
|
|
|
var selector = _selector || 'body'; |
|
1594
|
|
|
//Clear the |
|
1595
|
|
|
jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]').remove(); |
|
1596
|
|
|
return new Promise(function(_resolve, _reject){ |
|
1597
|
|
|
var resolve = _resolve; |
|
1598
|
|
|
var reject = _reject; |
|
1599
|
|
|
|
|
1600
|
|
|
mailvelope.getKeyring('egroupware').then(function(_keyring) |
|
1601
|
|
|
{ |
|
1602
|
|
|
_keyring.addSyncHandler(self.mailvelopeSyncHandlerObj); |
|
1603
|
|
|
|
|
1604
|
|
|
var options = { |
|
1605
|
|
|
restorePassword:restorePassword |
|
1606
|
|
|
}; |
|
1607
|
|
|
_keyring.restoreBackupContainer(selector, options).then(function(_restoreId){ |
|
1608
|
|
|
var $restore_selector = jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]'); |
|
1609
|
|
|
$restore_selector.css({position:'absolute', "z-index":1}); |
|
1610
|
|
|
resolve(_restoreId); |
|
1611
|
|
|
}, |
|
1612
|
|
|
function(_err){ |
|
1613
|
|
|
reject(_err); |
|
1614
|
|
|
}); |
|
1615
|
|
|
}, |
|
1616
|
|
|
function(_err) |
|
1617
|
|
|
{ |
|
1618
|
|
|
reject(_err); |
|
1619
|
|
|
}); |
|
1620
|
|
|
}); |
|
1621
|
|
|
} |
|
1622
|
|
|
|
|
1623
|
|
|
/** |
|
1624
|
|
|
* Create a dialog to show all backup/restore options |
|
1625
|
|
|
* |
|
1626
|
|
|
* @returns {undefined} |
|
1627
|
|
|
*/ |
|
1628
|
|
|
mailvelopeCreateBackupRestoreDialog() |
|
1629
|
|
|
{ |
|
1630
|
|
|
var self = this; |
|
1631
|
|
|
var appname = egw.app_name(); |
|
1632
|
|
|
var menu = [ |
|
1633
|
|
|
// Header row should be empty item 0 |
|
1634
|
|
|
{}, |
|
1635
|
|
|
// Restore Keyring item 1 |
|
1636
|
|
|
{label:"Restore key" ,image:"lock", onclick:"app."+appname+".mailvelopeCreateRestoreDialog('#_mvelo')"}, |
|
1637
|
|
|
// Restore pass phrase item 2 |
|
1638
|
|
|
{label:"Restore password",image:"password", onclick:"app."+appname+".mailvelopeCreateRestoreDialog('#_mvelo', true)"}, |
|
1639
|
|
|
// Delete backup Key item 3 |
|
1640
|
|
|
{label:"Delete backup", image:"delete", onclick:"app."+appname+".mailvelopeDeleteBackup"}, |
|
1641
|
|
|
// Backup Key item 4 |
|
1642
|
|
|
{label:"Backup Key", image:"save", onclick:"app."+appname+".mailvelopeCreateBackupDialog('#_mvelo', false)"} |
|
1643
|
|
|
]; |
|
1644
|
|
|
|
|
1645
|
|
|
var dialog = function(_content, _callback?) |
|
1646
|
|
|
{ |
|
1647
|
|
|
return et2_createWidget("dialog", { |
|
1648
|
|
|
callback: function(_button_id, _value) { |
|
1649
|
|
|
if (typeof _callback == "function") |
|
1650
|
|
|
{ |
|
1651
|
|
|
_callback.call(this, _button_id, _value.value); |
|
1652
|
|
|
} |
|
1653
|
|
|
}, |
|
1654
|
|
|
title: egw.lang('Backup/Restore'), |
|
1655
|
|
|
buttons:[{"button_id": 'close',"text": egw.lang('Close'), id: 'dialog[close]', image: 'cancelled', "default":true}], |
|
1656
|
|
|
value: { |
|
1657
|
|
|
content: { |
|
1658
|
|
|
menu:_content |
|
1659
|
|
|
} |
|
1660
|
|
|
}, |
|
1661
|
|
|
template: egw.webserverUrl+'/api/templates/default/pgp_backup_restore.xet', |
|
1662
|
|
|
class: "pgp_backup_restore", |
|
1663
|
|
|
modal:true |
|
1664
|
|
|
}); |
|
1665
|
|
|
}; |
|
1666
|
|
|
if (typeof mailvelope != 'undefined') |
|
1667
|
|
|
{ |
|
1668
|
|
|
mailvelope.getKeyring('egroupware').then(function(_keyring) |
|
1669
|
|
|
{ |
|
1670
|
|
|
self._mailvelopeBackupFileOperator(undefined, 'GET', function(_data){ |
|
1671
|
|
|
dialog(menu); |
|
1672
|
|
|
}, |
|
1673
|
|
|
function(){ |
|
1674
|
|
|
// Remove delete item |
|
1675
|
|
|
menu.splice(3,1); |
|
1676
|
|
|
menu[3]['onclick'] = "app."+appname+".mailvelopeCreateBackupDialog('#_mvelo', true)"; |
|
1677
|
|
|
dialog(menu); |
|
1678
|
|
|
}); |
|
1679
|
|
|
}, |
|
1680
|
|
|
function(){ |
|
1681
|
|
|
mailvelope.createKeyring('egroupware').then(function(){dialog(menu);}); |
|
1682
|
|
|
}); |
|
1683
|
|
|
} |
|
1684
|
|
|
else |
|
1685
|
|
|
{ |
|
1686
|
|
|
this.mailvelopeInstallationOffer(); |
|
1687
|
|
|
} |
|
1688
|
|
|
} |
|
1689
|
|
|
|
|
1690
|
|
|
/** |
|
1691
|
|
|
* Create a dialog and offers installation option for installing mailvelope plugin |
|
1692
|
|
|
* plus it offers a video tutorials to get the user morte familiar with mailvelope |
|
1693
|
|
|
*/ |
|
1694
|
|
|
mailvelopeInstallationOffer () |
|
1695
|
|
|
{ |
|
1696
|
|
|
var buttons = [ |
|
1697
|
|
|
{"text": egw.lang('Install'), id: 'install', image: 'check', "default":true}, |
|
1698
|
|
|
{"text": egw.lang('Close'), id:'close', image: 'cancelled'} |
|
1699
|
|
|
]; |
|
1700
|
|
|
var dialog = function(_content, _callback) |
|
1701
|
|
|
{ |
|
1702
|
|
|
return et2_createWidget("dialog", { |
|
1703
|
|
|
callback: function(_button_id, _value) { |
|
1704
|
|
|
if (typeof _callback == "function") |
|
1705
|
|
|
{ |
|
1706
|
|
|
_callback.call(this, _button_id, _value.value); |
|
1707
|
|
|
} |
|
1708
|
|
|
}, |
|
1709
|
|
|
title: egw.lang('PGP Encryption Installation'), |
|
1710
|
|
|
buttons: buttons, |
|
1711
|
|
|
dialog_type: 'info', |
|
1712
|
|
|
value: { |
|
1713
|
|
|
content: _content |
|
1714
|
|
|
}, |
|
1715
|
|
|
template: egw.webserverUrl+'/api/templates/default/pgp_installation.xet', |
|
1716
|
|
|
class: "pgp_installation", |
|
1717
|
|
|
modal: true |
|
1718
|
|
|
//resizable:false, |
|
1719
|
|
|
}); |
|
1720
|
|
|
}; |
|
1721
|
|
|
var content = [ |
|
1722
|
|
|
// Header row should be empty item 0 |
|
1723
|
|
|
{}, |
|
1724
|
|
|
{domain:this.egw.lang('Add your domain as "%1" in options to list of email providers and enable API.', |
|
1725
|
|
|
'*.'+this._mailvelopeDomain()), video:"test", control:"true"} |
|
1726
|
|
|
]; |
|
1727
|
|
|
|
|
1728
|
|
|
dialog(content, function(_button){ |
|
1729
|
|
|
if (_button == 'install') |
|
1730
|
|
|
{ |
|
1731
|
|
|
if (typeof chrome != 'undefined') |
|
1732
|
|
|
{ |
|
1733
|
|
|
// ATM we are not able to trigger mailvelope installation directly |
|
1734
|
|
|
// since the installation should be triggered from the extension |
|
1735
|
|
|
// owner validate website (mailvelope.com), therefore, we just redirect |
|
1736
|
|
|
// user to chrome webstore to install mailvelope from there. |
|
1737
|
|
|
window.open('https://chrome.google.com/webstore/detail/mailvelope/kajibbejlbohfaggdiogboambcijhkke'); |
|
1738
|
|
|
} |
|
1739
|
|
|
else if (typeof InstallTrigger != 'undefined' && InstallTrigger.enabled()) |
|
1740
|
|
|
{ |
|
1741
|
|
|
InstallTrigger.install({mailvelope:"https://download.mailvelope.com/releases/latest/mailvelope.firefox.xpi"}, |
|
1742
|
|
|
function(_url, _status){ |
|
1743
|
|
|
if (_status == 0) |
|
1744
|
|
|
{ |
|
1745
|
|
|
et2_dialog.alert(egw.lang('Mailvelope addon installation succeded. Now you may configure the options.')); |
|
1746
|
|
|
return; |
|
1747
|
|
|
} |
|
1748
|
|
|
else |
|
1749
|
|
|
{ |
|
1750
|
|
|
et2_dialog.alert(egw.lang('Mailvelope addon installation failed! Please try again.')); |
|
1751
|
|
|
} |
|
1752
|
|
|
}); |
|
1753
|
|
|
} |
|
1754
|
|
|
} |
|
1755
|
|
|
}); |
|
1756
|
|
|
} |
|
1757
|
|
|
|
|
1758
|
|
|
/** |
|
1759
|
|
|
* PGP begin and end tags |
|
1760
|
|
|
*/ |
|
1761
|
|
|
readonly begin_pgp_message: '-----BEGIN PGP MESSAGE-----'; |
|
1762
|
|
|
readonly end_pgp_message: '-----END PGP MESSAGE-----'; |
|
1763
|
|
|
|
|
1764
|
|
|
/** |
|
1765
|
|
|
* Mailvelope "egroupware" Keyring |
|
1766
|
|
|
*/ |
|
1767
|
|
|
mailvelope_keyring : any = undefined; |
|
1768
|
|
|
|
|
1769
|
|
|
/** |
|
1770
|
|
|
* jQuery selector for Mailvelope iframes in all browsers |
|
1771
|
|
|
*/ |
|
1772
|
|
|
readonly mailvelope_iframe_selector: 'iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]'; |
|
1773
|
|
|
|
|
1774
|
|
|
/** |
|
1775
|
|
|
* Open (or create) "egroupware" keyring and call callback with it |
|
1776
|
|
|
* |
|
1777
|
|
|
* @returns {Promise.<Keyring, Error>} Keyring or Error with message |
|
1778
|
|
|
*/ |
|
1779
|
|
|
mailvelopeOpenKeyring() |
|
1780
|
|
|
{ |
|
1781
|
|
|
var self = this; |
|
1782
|
|
|
|
|
1783
|
|
|
return new Promise(function(_resolve, _reject) |
|
1784
|
|
|
{ |
|
1785
|
|
|
if (self.mailvelope_keyring) _resolve(self.mailvelope_keyring); |
|
1786
|
|
|
|
|
1787
|
|
|
var resolve = _resolve; |
|
1788
|
|
|
var reject = _reject; |
|
1789
|
|
|
|
|
1790
|
|
|
mailvelope.getKeyring('egroupware').then(function(_keyring) |
|
1791
|
|
|
{ |
|
1792
|
|
|
self.mailvelope_keyring = _keyring; |
|
1793
|
|
|
|
|
1794
|
|
|
resolve(_keyring); |
|
1795
|
|
|
}, |
|
1796
|
|
|
function(_err) |
|
1797
|
|
|
{ |
|
1798
|
|
|
mailvelope.createKeyring('egroupware').then(function(_keyring) |
|
1799
|
|
|
{ |
|
1800
|
|
|
self.mailvelope_keyring = _keyring; |
|
1801
|
|
|
var mvelo_settings_selector = self.mailvelope_iframe_selector |
|
1802
|
|
|
.split(',').map(function(_val){return 'body>'+_val;}).join(','); |
|
1803
|
|
|
|
|
1804
|
|
|
mailvelope.createSettingsContainer('body', _keyring, { |
|
1805
|
|
|
email: self.egw.user('account_email'), |
|
1806
|
|
|
fullName: self.egw.user('account_fullname') |
|
1807
|
|
|
}).then(function() |
|
1808
|
|
|
{ |
|
1809
|
|
|
// make only Mailvelope settings dialog visible |
|
1810
|
|
|
jQuery(mvelo_settings_selector).css({position: 'absolute', top: 0}); |
|
1811
|
|
|
// add a close button, so we know when to offer storing public key to AB |
|
1812
|
|
|
jQuery('<button class="et2_button et2_button_text" id="mailvelope_close_settings">'+self.egw.lang('Close')+'</button>') |
|
1813
|
|
|
.css({position: 'absolute', top: 8, right: 8, "z-index":2}) |
|
1814
|
|
|
.click(function() |
|
1815
|
|
|
{ |
|
1816
|
|
|
// try fetching public key, to check user created onw |
|
1817
|
|
|
self.mailvelope_keyring.exportOwnPublicKey(self.egw.user('account_email')).then(function(_pubKey) |
|
1818
|
|
|
{ |
|
1819
|
|
|
// CreateBackupDialog |
|
1820
|
|
|
self.mailvelopeCreateBackupDialog().then(function(_popupId){ |
|
1821
|
|
|
jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]').css({position:'absolute', "z-index":1}); |
|
1822
|
|
|
}, |
|
1823
|
|
|
function(_err){ |
|
1824
|
|
|
egw.message(_err); |
|
1825
|
|
|
}); |
|
1826
|
|
|
|
|
1827
|
|
|
// if yes, hide settings dialog |
|
1828
|
|
|
jQuery(mvelo_settings_selector).each(function(index,item : any){ |
|
1829
|
|
|
if (!item.src.match(/keyBackupDialog.html/,'ig')) item.remove(); |
|
1830
|
|
|
}); |
|
1831
|
|
|
jQuery('button#mailvelope_close_settings').remove(); |
|
1832
|
|
|
|
|
1833
|
|
|
// offer user to store his public key to AB for other users to find |
|
1834
|
|
|
var buttons = [ |
|
1835
|
|
|
{button_id: 2, text: 'Yes', id: 'dialog[yes]', image: 'check', default: true}, |
|
1836
|
|
|
{button_id: 3, text : 'No', id: 'dialog[no]', image: 'cancelled'} |
|
1837
|
|
|
]; |
|
1838
|
|
|
if (egw.user('apps').admin) |
|
1839
|
|
|
{ |
|
1840
|
|
|
buttons.unshift({ |
|
1841
|
|
|
button_id: 5, text: 'Yes and allow non-admin users to do that too (recommended)', |
|
1842
|
|
|
id: 'dialog[yes_allow]', image: 'check', default: true |
|
1843
|
|
|
}); |
|
1844
|
|
|
delete buttons[1].default; |
|
1845
|
|
|
} |
|
1846
|
|
|
et2_dialog.show_dialog(function (_button_id) |
|
1847
|
|
|
{ |
|
1848
|
|
|
if (_button_id != et2_dialog.NO_BUTTON ) |
|
1849
|
|
|
{ |
|
1850
|
|
|
var keys = {}; |
|
1851
|
|
|
keys[self.egw.user('account_id')] = _pubKey; |
|
1852
|
|
|
self.egw.json('addressbook.addressbook_bo.ajax_set_pgp_keys', |
|
1853
|
|
|
[keys, _button_id != et2_dialog.YES_BUTTON ? true : undefined]).sendRequest() |
|
1854
|
|
|
.then(function(_data) |
|
1855
|
|
|
{ |
|
1856
|
|
|
self.egw.message(_data.response['0'].data); |
|
1857
|
|
|
}); |
|
1858
|
|
|
} |
|
1859
|
|
|
}, |
|
1860
|
|
|
self.egw.lang('It is recommended to store your public key in addressbook, so other users can write you encrypted mails.'), |
|
1861
|
|
|
self.egw.lang('Store your public key in Addressbook?'), |
|
1862
|
|
|
{}, buttons, et2_dialog.QUESTION_MESSAGE, undefined, self.egw); |
|
1863
|
|
|
}, |
|
1864
|
|
|
function(_err){ |
|
1865
|
|
|
self.egw.message(_err.message+"\n\n"+ |
|
1866
|
|
|
self.egw.lang("You will NOT be able to send or receive encrypted mails before completing that step!"), 'error'); |
|
1867
|
|
|
}); |
|
1868
|
|
|
}) |
|
1869
|
|
|
.appendTo('body'); |
|
1870
|
|
|
}); |
|
1871
|
|
|
resolve(_keyring); |
|
1872
|
|
|
}, |
|
1873
|
|
|
function(_err) |
|
1874
|
|
|
{ |
|
1875
|
|
|
reject(_err); |
|
1876
|
|
|
}); |
|
1877
|
|
|
}); |
|
1878
|
|
|
}); |
|
1879
|
|
|
} |
|
1880
|
|
|
|
|
1881
|
|
|
/** |
|
1882
|
|
|
* Mailvelope uses Domain without first part: eg. "stylite.de" for "egw.stylite.de" |
|
1883
|
|
|
* |
|
1884
|
|
|
* @returns {string} |
|
1885
|
|
|
*/ |
|
1886
|
|
|
_mailvelopeDomain() |
|
1887
|
|
|
{ |
|
1888
|
|
|
var parts = document.location.hostname.split('.'); |
|
1889
|
|
|
if (parts.length > 1) parts.shift(); |
|
1890
|
|
|
return parts.join('.'); |
|
1891
|
|
|
} |
|
1892
|
|
|
|
|
1893
|
|
|
/** |
|
1894
|
|
|
* Check if we have a key for all recipients |
|
1895
|
|
|
* |
|
1896
|
|
|
* @param {Array} _recipients |
|
1897
|
|
|
* @returns {Promise.<Array, Error>} Array of recipients or Error with recipients without key |
|
1898
|
|
|
*/ |
|
1899
|
|
|
mailvelopeGetCheckRecipients(_recipients) |
|
1900
|
|
|
{ |
|
1901
|
|
|
// replace rfc822 addresses with raw email, as Mailvelop does not like them and lowercase all email |
|
1902
|
|
|
var rfc822_preg = /<([^'" <>]+)>$/; |
|
1903
|
|
|
var recipients = _recipients.map(function(_recipient) |
|
1904
|
|
|
{ |
|
1905
|
|
|
var matches = _recipient.match(rfc822_preg); |
|
1906
|
|
|
return matches ? matches[1].toLowerCase() : _recipient.toLowerCase(); |
|
1907
|
|
|
}); |
|
1908
|
|
|
|
|
1909
|
|
|
// check if we have keys for all recipients |
|
1910
|
|
|
var self = this; |
|
1911
|
|
|
return new Promise(function(_resolve, _reject) |
|
1912
|
|
|
{ |
|
1913
|
|
|
var resolve = _resolve; |
|
1914
|
|
|
var reject = _reject; |
|
1915
|
|
|
self.mailvelopeOpenKeyring().then(function(_keyring : any) |
|
1916
|
|
|
{ |
|
1917
|
|
|
var keyring = _keyring; |
|
1918
|
|
|
_keyring.validKeyForAddress(recipients).then(function(_status) |
|
1919
|
|
|
{ |
|
1920
|
|
|
var no_key = []; |
|
1921
|
|
|
for(var email in _status) |
|
1922
|
|
|
{ |
|
1923
|
|
|
if (!_status[email]) no_key.push(email); |
|
1924
|
|
|
} |
|
1925
|
|
|
if (no_key.length) |
|
1926
|
|
|
{ |
|
1927
|
|
|
// server addressbook on server for missing public keys |
|
1928
|
|
|
self.egw.json('addressbook.addressbook_bo.ajax_get_pgp_keys', [no_key]).sendRequest().then(function(_data) |
|
1929
|
|
|
{ |
|
1930
|
|
|
var data = _data.response['0'].data; |
|
1931
|
|
|
var promises = []; |
|
1932
|
|
|
for(var email in data) |
|
1933
|
|
|
{ |
|
1934
|
|
|
promises.push(keyring.importPublicKey(data[email]).then(function(_result) |
|
1935
|
|
|
{ |
|
1936
|
|
|
if (_result == 'IMPORTED' || _result == 'UPDATED') |
|
1937
|
|
|
{ |
|
1938
|
|
|
no_key.splice(no_key.indexOf(email),1); |
|
1939
|
|
|
} |
|
1940
|
|
|
})); |
|
1941
|
|
|
} |
|
1942
|
|
|
Promise.all(promises).then(function() |
|
1943
|
|
|
{ |
|
1944
|
|
|
if (no_key.length) |
|
1945
|
|
|
{ |
|
1946
|
|
|
reject(new Error(self.egw.lang('No key for recipient:')+' '+no_key.join(', '))); |
|
1947
|
|
|
} |
|
1948
|
|
|
else |
|
1949
|
|
|
{ |
|
1950
|
|
|
resolve(recipients); |
|
1951
|
|
|
} |
|
1952
|
|
|
}); |
|
1953
|
|
|
}); |
|
1954
|
|
|
} |
|
1955
|
|
|
else |
|
1956
|
|
|
{ |
|
1957
|
|
|
resolve(recipients); |
|
1958
|
|
|
} |
|
1959
|
|
|
}); |
|
1960
|
|
|
}, |
|
1961
|
|
|
function(_err) |
|
1962
|
|
|
{ |
|
1963
|
|
|
reject(_err); |
|
1964
|
|
|
}); |
|
1965
|
|
|
}); |
|
1966
|
|
|
} |
|
1967
|
|
|
|
|
1968
|
|
|
/** |
|
1969
|
|
|
* Check if the share action is enabled for this entry |
|
1970
|
|
|
* |
|
1971
|
|
|
* @param {egwAction} _action |
|
1972
|
|
|
* @param {egwActionObject[]} _entries |
|
1973
|
|
|
* @param {egwActionObject} _target |
|
1974
|
|
|
* @returns {boolean} if action is enabled |
|
1975
|
|
|
*/ |
|
1976
|
|
|
is_share_enabled(_action, _entries, _target) |
|
1977
|
|
|
{ |
|
1978
|
|
|
return true; |
|
1979
|
|
|
} |
|
1980
|
|
|
/** |
|
1981
|
|
|
* create a share-link for the given entry |
|
1982
|
|
|
* |
|
1983
|
|
|
* @param {egwAction} _action egw actions |
|
1984
|
|
|
* @param {egwActionObject[]} _senders selected nm row |
|
1985
|
|
|
* @param {egwActionObject} _target Drag source. Not used here. |
|
1986
|
|
|
* @param {Boolean} _writable Allow edit access from the share. |
|
1987
|
|
|
* @param {Boolean} _files Allow access to files from the share. |
|
1988
|
|
|
* @param {Function} _callback Callback with results |
|
1989
|
|
|
* @returns {Boolean} returns false if not successful |
|
1990
|
|
|
*/ |
|
1991
|
|
|
share_link(_action, _senders, _target, _writable, _files, _callback){ |
|
1992
|
|
|
var path = _senders[0].id; |
|
1993
|
|
|
if(!path) |
|
1994
|
|
|
{ |
|
1995
|
|
|
return this.egw.message(this.egw.lang('Missing share path. Unable to create share.'), 'error'); |
|
1996
|
|
|
} |
|
1997
|
|
|
switch(_action.id) |
|
1998
|
|
|
{ |
|
1999
|
|
|
case 'shareFilemanager': |
|
2000
|
|
|
// Sharing a link to just files in filemanager |
|
2001
|
|
|
var id = path.split('::'); |
|
2002
|
|
|
path = '/apps/'+ id[0] + '/' + id[1]; |
|
2003
|
|
|
} |
|
2004
|
|
|
if(typeof _writable === 'undefined' && _action.parent && _action.parent.getActionById('shareWritable')) |
|
2005
|
|
|
{ |
|
2006
|
|
|
_writable = _action.parent.getActionById('shareWritable').checked || false; |
|
2007
|
|
|
} |
|
2008
|
|
|
if(typeof _files === 'undefined' && _action.parent && _action.parent.getActionById('shareFiles')) |
|
2009
|
|
|
{ |
|
2010
|
|
|
_files = _action.parent.getActionById('shareFiles').checked || false; |
|
2011
|
|
|
} |
|
2012
|
|
|
|
|
2013
|
|
|
return egw.json('EGroupware\\Api\\Sharing::ajax_create', [_action.id, path, _writable, _files], |
|
2014
|
|
|
_callback ? _callback : this._share_link_callback, this, true, this).sendRequest(); |
|
2015
|
|
|
} |
|
2016
|
|
|
|
|
2017
|
|
|
share_merge(_action, _senders, _target) |
|
2018
|
|
|
{ |
|
2019
|
|
|
var parent = _action.parent.parent; |
|
2020
|
|
|
var _writable = false; |
|
2021
|
|
|
var _files = false; |
|
2022
|
|
|
if(parent && parent.getActionById('shareWritable')) |
|
2023
|
|
|
{ |
|
2024
|
|
|
_writable = parent.getActionById('shareWritable').checked || false; |
|
2025
|
|
|
} |
|
2026
|
|
|
if(parent && parent.getActionById('shareFiles')) |
|
2027
|
|
|
{ |
|
2028
|
|
|
_files = parent.getActionById('shareFiles').checked || false; |
|
2029
|
|
|
} |
|
2030
|
|
|
|
|
2031
|
|
|
// Share only works on one at a time |
|
2032
|
|
|
var promises = []; |
|
2033
|
|
|
for(var i = 0; i < _senders.length; i++) |
|
2034
|
|
|
{ |
|
2035
|
|
|
promises.push(new Promise(function(resolve, reject) { |
|
2036
|
|
|
this.share_link(_action, [_senders[i]], _target, _writable, _files, resolve); |
|
2037
|
|
|
}.bind(this))); |
|
2038
|
|
|
} |
|
2039
|
|
|
|
|
2040
|
|
|
// But merge into email can handle several |
|
2041
|
|
|
Promise.all(promises.map(function(p){p.catch(function(e){console.log(e)})})) |
|
2042
|
|
|
.then(function(values) { |
|
2043
|
|
|
// Process document after all shares created |
|
2044
|
|
|
return nm_action(_action, _senders, _target); |
|
2045
|
|
|
}); |
|
2046
|
|
|
} |
|
2047
|
|
|
|
|
2048
|
|
|
/** |
|
2049
|
|
|
* Share-link callback |
|
2050
|
|
|
* @param {object} _data |
|
2051
|
|
|
*/ |
|
2052
|
|
|
_share_link_callback(_data) { |
|
2053
|
|
|
if (_data.msg || _data.share_link) window.egw_refresh(_data.msg, this.appname); |
|
2054
|
|
|
|
|
2055
|
|
|
var copy_link_to_clipboard = function(evt){ |
|
2056
|
|
|
var $target = jQuery(evt.target); |
|
2057
|
|
|
$target.select(); |
|
2058
|
|
|
try { |
|
2059
|
|
|
var successful = document.execCommand('copy'); |
|
2060
|
|
|
if (successful) |
|
2061
|
|
|
{ |
|
2062
|
|
|
egw.message('Share link copied into clipboard'); |
|
2063
|
|
|
return true; |
|
2064
|
|
|
} |
|
2065
|
|
|
} |
|
2066
|
|
|
catch (e) {} |
|
2067
|
|
|
egw.message('Failed to copy the link!'); |
|
2068
|
|
|
}; |
|
2069
|
|
|
jQuery("body").on("click", "[name=share_link]", copy_link_to_clipboard); |
|
2070
|
|
|
et2_createWidget("dialog", { |
|
2071
|
|
|
callback: function( button_id, value) { |
|
2072
|
|
|
jQuery("body").off("click", "[name=share_link]", copy_link_to_clipboard); |
|
2073
|
|
|
return true; |
|
2074
|
|
|
}, |
|
2075
|
|
|
title: _data.title ? _data.title : egw.lang("%1 Share Link", _data.writable ? egw.lang("Writable"): egw.lang("Readonly")), |
|
2076
|
|
|
template: _data.template, |
|
2077
|
|
|
width: 450, |
|
2078
|
|
|
value: {content:{ "share_link": _data.share_link }} |
|
2079
|
|
|
}); |
|
2080
|
|
|
} |
|
2081
|
|
|
} |
|
2082
|
|
|
|